This commit is contained in:
robin
2026-03-13 14:25:13 +08:00
parent a25a474d6a
commit afbaaa869c
95 changed files with 4591 additions and 2578 deletions

View File

@@ -128,13 +128,11 @@ function build() {
# copy files # copy files
echo "copying ..." echo "copying ..."
if [ ! -d "$DIST" ]; then rm -rf "$DIST"
mkdir "$DIST" mkdir -p "$DIST"/bin
mkdir "$DIST"/bin mkdir -p "$DIST"/configs
mkdir "$DIST"/configs mkdir -p "$DIST"/logs
mkdir "$DIST"/logs mkdir -p "$DIST"/data
mkdir "$DIST"/data
fi
cp "$ROOT"/configs/api.template.yaml "$DIST"/configs/ cp "$ROOT"/configs/api.template.yaml "$DIST"/configs/
cp "$ROOT"/configs/db.template.yaml "$DIST"/configs/ cp "$ROOT"/configs/db.template.yaml "$DIST"/configs/
# 复制 EdgeCommon 的配置文件(如果存在) # 复制 EdgeCommon 的配置文件(如果存在)
@@ -148,42 +146,6 @@ function build() {
rm -f "$DIST"/deploy/.gitignore rm -f "$DIST"/deploy/.gitignore
cp -R "$ROOT"/installers "$DIST"/ cp -R "$ROOT"/installers "$DIST"/
# copy fluent-bit templates and local packages from repo root
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/deploy/fluent-bit"
if [ -d "$FLUENT_ROOT" ]; then
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
FLUENT_FILES=(
"fluent-bit.conf"
"fluent-bit-dns.conf"
"fluent-bit-https.conf"
"fluent-bit-dns-https.conf"
"fluent-bit-windows.conf"
"fluent-bit-windows-https.conf"
"parsers.conf"
"clickhouse-upstream.conf"
"clickhouse-upstream-windows.conf"
"logrotate.conf"
"README.md"
)
for file in "${FLUENT_FILES[@]}"; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ -d "$FLUENT_ROOT/packages" ]; then
cp -R "$FLUENT_ROOT/packages" "$FLUENT_DIST/"
fi
# remove local runtime artifacts if present
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
fi
# building edge installer # building edge installer
echo "building node installer ..." echo "building node installer ..."
architects=("amd64") architects=("amd64")

View File

@@ -47,6 +47,33 @@ type HTTPDNSAccessLogListFilter struct {
Size int64 Size int64
} }
type HTTPDNSAccessLogHourlyStat struct {
Hour string
CountRequests int64
}
type HTTPDNSAccessLogDailyStat struct {
Day string
CountRequests int64
}
type HTTPDNSAccessLogTopAppStat struct {
AppId string
AppName string
CountRequests int64
}
type HTTPDNSAccessLogTopDomainStat struct {
Domain string
CountRequests int64
}
type HTTPDNSAccessLogTopNodeStat struct {
ClusterId uint32
NodeId uint32
CountRequests int64
}
type HTTPDNSAccessLogsStore struct { type HTTPDNSAccessLogsStore struct {
client *Client client *Client
} }
@@ -176,6 +203,155 @@ func (s *HTTPDNSAccessLogsStore) List(ctx context.Context, f HTTPDNSAccessLogLis
return result, nil return result, nil
} }
func (s *HTTPDNSAccessLogsStore) FindHourlyStats(ctx context.Context, hourFrom string, hourTo string) ([]*HTTPDNSAccessLogHourlyStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
query := fmt.Sprintf(
"SELECT formatDateTime(toDateTime(created_at), '%%Y%%m%%d%%H') AS hour, count() AS count_requests FROM %s WHERE formatDateTime(toDateTime(created_at), '%%Y%%m%%d%%H') BETWEEN '%s' AND '%s' GROUP BY hour ORDER BY hour ASC",
s.tableName(),
escapeString(hourFrom),
escapeString(hourTo),
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogHourlyStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogHourlyStat{
Hour: toString(row["hour"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) FindDailyStats(ctx context.Context, dayFrom string, dayTo string) ([]*HTTPDNSAccessLogDailyStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
query := fmt.Sprintf(
"SELECT day, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY day ORDER BY day ASC",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogDailyStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogDailyStat{
Day: toString(row["day"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopApps(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopAppStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT app_id, max(app_name) AS app_name, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY app_id ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopAppStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopAppStat{
AppId: toString(row["app_id"]),
AppName: toString(row["app_name"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopDomains(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopDomainStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT domain, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' AND domain != '' GROUP BY domain ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopDomainStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopDomainStat{
Domain: toString(row["domain"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopNodes(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopNodeStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT min(cluster_id) AS cluster_id, node_id, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY node_id ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopNodeStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopNodeStat{
ClusterId: uint32(toInt64(row["cluster_id"])),
NodeId: uint32(toInt64(row["node_id"])),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func HTTPDNSRowToPB(row *HTTPDNSAccessLogRow) *pb.HTTPDNSAccessLog { func HTTPDNSRowToPB(row *HTTPDNSAccessLogRow) *pb.HTTPDNSAccessLog {
if row == nil { if row == nil {
return nil return nil

View File

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

View File

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

View File

@@ -0,0 +1,138 @@
package models
import (
"github.com/iwind/TeaGo/dbs"
)
type HTTPDNSAccessLogHourlyStat struct {
Hour string `field:"hour"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogDailyStat struct {
Day string `field:"day"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopAppStat struct {
AppId string `field:"appId"`
AppName string `field:"appName"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopDomainStat struct {
Domain string `field:"domain"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopNodeStat struct {
ClusterId uint32 `field:"clusterId"`
NodeId uint32 `field:"nodeId"`
CountRequests int64 `field:"countRequests"`
}
func (this *HTTPDNSAccessLogDAO) FindHourlyStats(tx *dbs.Tx, hourFrom string, hourTo string) (result []*HTTPDNSAccessLogHourlyStat, err error) {
rows, _, err := this.Query(tx).
Result("CONCAT(day, LPAD(HOUR(FROM_UNIXTIME(createdAt)),2,'0')) AS hour", "COUNT(*) AS countRequests").
Where("CONCAT(day, LPAD(HOUR(FROM_UNIXTIME(createdAt)),2,'0')) BETWEEN :hourFrom AND :hourTo").
Param("hourFrom", hourFrom).
Param("hourTo", hourTo).
Group("hour").
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogHourlyStat{
Hour: row.GetString("hour"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) FindDailyStats(tx *dbs.Tx, dayFrom string, dayTo string) (result []*HTTPDNSAccessLogDailyStat, err error) {
rows, _, err := this.Query(tx).
Result("day", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("day").
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogDailyStat{
Day: row.GetString("day"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopApps(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopAppStat, err error) {
rows, _, err := this.Query(tx).
Result("appId", "MAX(appName) AS appName", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("appId").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopAppStat{
AppId: row.GetString("appId"),
AppName: row.GetString("appName"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopDomains(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopDomainStat, err error) {
rows, _, err := this.Query(tx).
Result("domain", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Where("domain != ''").
Group("domain").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopDomainStat{
Domain: row.GetString("domain"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopNodes(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopNodeStat, err error) {
rows, _, err := this.Query(tx).
Result("MIN(clusterId) AS clusterId", "nodeId", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("nodeId").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopNodeStat{
ClusterId: row.GetUint32("clusterId"),
NodeId: row.GetUint32("nodeId"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}

View File

@@ -113,3 +113,11 @@ func (this *HTTPDNSDomainDAO) ListEnabledDomainsWithAppId(tx *dbs.Tx, appDbId in
_, err = query.Slice(&result).FindAll() _, err = query.Slice(&result).FindAll()
return return
} }
func (this *HTTPDNSDomainDAO) CountEnabledDomains(tx *dbs.Tx, appDbId int64) (int64, error) {
query := this.Query(tx).State(HTTPDNSDomainStateEnabled)
if appDbId > 0 {
query = query.Attr("appId", appDbId)
}
return query.Count()
}

View File

@@ -221,6 +221,41 @@ func (this *NodeValueDAO) ListValuesForNSNodes(tx *dbs.Tx, item string, key stri
return return
} }
// ListValuesForHTTPDNSNodes 列出HTTPDNS节点相关的平均数据
func (this *NodeValueDAO) ListValuesForHTTPDNSNodes(tx *dbs.Tx, item string, key string, timeRange nodeconfigs.NodeValueRange) (result []*NodeValue, err error) {
query := this.Query(tx).
Attr("role", "httpdns").
Attr("item", item).
Result("AVG(JSON_EXTRACT(value, '$." + key + "')) AS value, MIN(createdAt) AS createdAt")
switch timeRange {
// TODO 支持更多的时间范围
case nodeconfigs.NodeValueRangeMinute:
fromMinute := timeutil.FormatTime("YmdHi", time.Now().Unix()-3600) // 一个小时之前的
query.Gte("minute", fromMinute)
query.Result("minute")
query.Group("minute")
default:
err = errors.New("invalid 'range' value: '" + timeRange + "'")
return
}
_, err = query.Slice(&result).
FindAll()
if err != nil {
return nil, err
}
for _, nodeValue := range result {
nodeValue.Value, _ = json.Marshal(maps.Map{
key: types.Float32(string(nodeValue.Value)),
})
}
return
}
// SumAllNodeValues 计算所有节点的某项参数值 // SumAllNodeValues 计算所有节点的某项参数值
func (this *NodeValueDAO) SumAllNodeValues(tx *dbs.Tx, role string, item nodeconfigs.NodeValueItem, param string, duration int32, durationUnit nodeconfigs.NodeValueDurationUnit) (total float64, avg float64, max float64, err error) { func (this *NodeValueDAO) SumAllNodeValues(tx *dbs.Tx, role string, item nodeconfigs.NodeValueItem, param string, duration int32, durationUnit nodeconfigs.NodeValueDurationUnit) (total float64, avg float64, max float64, err error) {
if duration <= 0 { if duration <= 0 {

View File

@@ -1,889 +0,0 @@
package installers
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
slashpath "path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/Tea"
)
const (
fluentBitConfigDir = "/etc/fluent-bit"
fluentBitStorageDir = "/var/lib/fluent-bit/storage"
fluentBitMainConfigFile = "/etc/fluent-bit/fluent-bit.conf"
fluentBitParsersFile = "/etc/fluent-bit/parsers.conf"
fluentBitManagedMetaFile = "/etc/fluent-bit/.edge-managed.json"
fluentBitManagedEnvFile = "/etc/fluent-bit/.edge-managed.env"
fluentBitDropInDir = "/etc/systemd/system/fluent-bit.service.d"
fluentBitDropInFile = "/etc/systemd/system/fluent-bit.service.d/edge-managed.conf"
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"
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")
var fluentBitPackageFileMapping = map[string]string{
"ubuntu22.04-amd64": "fluent-bit_4.2.2_amd64.deb",
"ubuntu22.04-arm64": "fluent-bit_4.2.2_arm64.deb",
"amzn2023-amd64": "fluent-bit-4.2.2-1.x86_64.rpm",
"amzn2023-arm64": "fluent-bit-4.2.2-1.aarch64.rpm",
}
type fluentBitManagedMeta struct {
Roles []string `json:"roles"`
Hash string `json:"hash"`
UpdatedAt int64 `json:"updatedAt"`
SourceVersion string `json:"sourceVersion"`
}
type fluentBitDesiredConfig struct {
Roles []string
ClickHouse *systemconfigs.ClickHouseSetting
HTTPPathPattern string
DNSPathPattern string
HTTPDNSPathPattern string
}
// SetupFluentBit 安装并托管 Fluent Bit 配置(离线包 + 平台渲染配置)。
func (this *BaseInstaller) SetupFluentBit(role nodeconfigs.NodeRole) error {
if this.client == nil {
return errors.New("ssh client is nil")
}
uname := this.uname()
if !strings.Contains(uname, "Linux") {
return nil
}
tempDir := strings.TrimRight(this.client.UserHome(), "/") + "/.edge-fluent-bit"
_, _, _ = this.client.Exec("mkdir -p " + shQuote(tempDir))
defer func() {
_, _, _ = this.client.Exec("rm -rf " + shQuote(tempDir))
}()
if err := this.ensureFluentBitInstalled(tempDir); err != nil {
return err
}
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitConfigDir) + " " + shQuote(fluentBitStorageDir))
if err != nil {
return fmt.Errorf("prepare fluent-bit directories failed: %w, stderr: %s", err, stderr)
}
parserContent, err := this.readLocalParsersContent()
if err != nil {
return err
}
existingMeta, err := this.readManagedMeta()
if err != nil {
return err
}
mergedRoles, err := mergeManagedRoles(existingMeta, role)
if err != nil {
return err
}
desiredConfig, err := this.buildDesiredFluentBitConfig(mergedRoles)
if err != nil {
return err
}
configChanged, err := this.applyManagedConfig(tempDir, desiredConfig, parserContent, existingMeta)
if err != nil {
return err
}
binPath, err := this.lookupFluentBitBinPath()
if err != nil {
return err
}
if err := this.ensureFluentBitService(tempDir, binPath, configChanged); err != nil {
return err
}
return nil
}
func (this *BaseInstaller) ensureFluentBitInstalled(tempDir string) error {
binPath, _ := this.lookupFluentBitBinPath()
if binPath != "" {
return nil
}
platformKey, packageName, arch, err := this.detectRemotePlatformAndPackage()
if err != nil {
return fmt.Errorf("detect fluent-bit platform failed: %w", err)
}
if err := this.installFluentBitFromLocalPackage(tempDir, arch, packageName); err != nil {
if errors.Is(err, errFluentBitLocalPackageNotFound) {
expectedPath := filepath.Join("deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch, packageName)
return fmt.Errorf("install fluent-bit failed: local package missing for platform '%s', expected '%s'", platformKey, expectedPath)
}
return fmt.Errorf("install fluent-bit from local package failed: %w", err)
}
binPath, err = this.lookupFluentBitBinPath()
if err != nil {
return err
}
if binPath == "" {
return errors.New("fluent-bit binary not found after local package install")
}
_, stderr, err := this.client.Exec(binPath + " --version")
if err != nil {
return fmt.Errorf("verify fluent-bit version failed: %w, stderr: %s", err, stderr)
}
return nil
}
func (this *BaseInstaller) installFluentBitFromLocalPackage(tempDir string, arch string, packageName string) error {
packageDir := filepath.Join(Tea.Root, "deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch)
localPackagePath := filepath.Join(packageDir, packageName)
if _, err := os.Stat(localPackagePath); err != nil {
if os.IsNotExist(err) {
return errFluentBitLocalPackageNotFound
}
return fmt.Errorf("check local package failed: %w", err)
}
remotePackagePath := tempDir + "/" + filepath.Base(localPackagePath)
if err := this.client.Copy(localPackagePath, remotePackagePath, 0644); err != nil {
return fmt.Errorf("upload local package failed: %w", err)
}
var installCmd string
lowerName := strings.ToLower(localPackagePath)
switch {
case strings.HasSuffix(lowerName, ".deb"):
installCmd = "dpkg -i " + shQuote(remotePackagePath)
case strings.HasSuffix(lowerName, ".rpm"):
installCmd = "rpm -Uvh --force " + shQuote(remotePackagePath) + " || rpm -ivh --force " + shQuote(remotePackagePath)
case strings.HasSuffix(lowerName, ".tar.gz") || strings.HasSuffix(lowerName, ".tgz"):
extractDir := tempDir + "/extract"
installCmd = "rm -rf " + shQuote(extractDir) + "; mkdir -p " + shQuote(extractDir) + "; tar -xzf " + shQuote(remotePackagePath) + " -C " + shQuote(extractDir) + "; " +
"bin=$(find " + shQuote(extractDir) + " -type f -name fluent-bit | head -n 1); " +
"if [ -z \"$bin\" ]; then exit 3; fi; " +
"mkdir -p /opt/fluent-bit/bin /usr/local/bin; " +
"install -m 0755 \"$bin\" /opt/fluent-bit/bin/fluent-bit; " +
"ln -sf /opt/fluent-bit/bin/fluent-bit /usr/local/bin/fluent-bit"
default:
return fmt.Errorf("unsupported local package format: %s", packageName)
}
_, stderr, err := this.client.Exec(installCmd)
if err != nil {
return fmt.Errorf("install fluent-bit local package '%s' failed: %w, stderr: %s", filepath.Base(localPackagePath), err, stderr)
}
return nil
}
func (this *BaseInstaller) detectRemotePlatformAndPackage() (platformKey string, packageName string, arch string, err error) {
arch, err = this.detectRemoteLinuxArch()
if err != nil {
return "", "", "", err
}
releaseData, stderr, err := this.client.Exec("cat /etc/os-release")
if err != nil {
return "", "", "", fmt.Errorf("read /etc/os-release failed: %w, stderr: %s", err, stderr)
}
if strings.TrimSpace(releaseData) == "" {
return "", "", "", errors.New("/etc/os-release is empty")
}
releaseMap := parseOSRelease(releaseData)
id := strings.ToLower(strings.TrimSpace(releaseMap["ID"]))
versionID := strings.TrimSpace(releaseMap["VERSION_ID"])
var distro string
switch {
case id == "ubuntu" && strings.HasPrefix(versionID, "22.04"):
distro = "ubuntu22.04"
case id == "amzn" && strings.HasPrefix(versionID, "2023"):
distro = "amzn2023"
default:
return "", "", "", fmt.Errorf("unsupported linux platform id='%s' version='%s'", id, versionID)
}
platformKey = distro + "-" + arch
packageName, ok := fluentBitPackageFileMapping[platformKey]
if !ok {
return "", "", "", fmt.Errorf("no local package mapping for platform '%s'", platformKey)
}
return platformKey, packageName, arch, nil
}
func parseOSRelease(content string) map[string]string {
result := map[string]string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
continue
}
parts := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, "\"")
result[key] = value
}
return result
}
func (this *BaseInstaller) detectRemoteLinuxArch() (string, error) {
stdout, stderr, err := this.client.Exec("uname -m")
if err != nil {
return "", fmt.Errorf("detect remote arch failed: %w, stderr: %s", err, stderr)
}
arch := strings.ToLower(strings.TrimSpace(stdout))
switch arch {
case "x86_64", "amd64":
return "amd64", nil
case "aarch64", "arm64":
return "arm64", nil
default:
return arch, nil
}
}
func (this *BaseInstaller) lookupFluentBitBinPath() (string, error) {
stdout, stderr, err := this.client.Exec("if command -v fluent-bit >/dev/null 2>&1; then command -v fluent-bit; elif [ -x " + fluentBitDefaultBinPath + " ]; then echo " + fluentBitDefaultBinPath + "; fi")
if err != nil {
return "", fmt.Errorf("lookup fluent-bit binary failed: %w, stderr: %s", err, stderr)
}
return strings.TrimSpace(stdout), nil
}
func (this *BaseInstaller) readLocalParsersContent() (string, error) {
parsersPath := filepath.Join(Tea.Root, "deploy", "fluent-bit", "parsers.conf")
data, err := os.ReadFile(parsersPath)
if err != nil {
return "", fmt.Errorf("read local parsers config failed: %w", err)
}
return string(data), nil
}
func (this *BaseInstaller) readManagedMeta() (*fluentBitManagedMeta, error) {
exists, err := this.remoteFileExists(fluentBitManagedMetaFile)
if err != nil {
return nil, err
}
if !exists {
return nil, nil
}
content, stderr, err := this.client.Exec("cat " + shQuote(fluentBitManagedMetaFile))
if err != nil {
return nil, fmt.Errorf("read fluent-bit managed metadata failed: %w, stderr: %s", err, stderr)
}
if strings.TrimSpace(content) == "" {
return nil, nil
}
meta := &fluentBitManagedMeta{}
if err := json.Unmarshal([]byte(content), meta); err != nil {
return nil, fmt.Errorf("decode fluent-bit managed metadata failed: %w", err)
}
meta.Roles = normalizeRoles(meta.Roles)
return meta, nil
}
func mergeManagedRoles(meta *fluentBitManagedMeta, role nodeconfigs.NodeRole) ([]string, error) {
roleName, err := mapNodeRole(role)
if err != nil {
return nil, err
}
roleSet := map[string]struct{}{}
if meta != nil {
for _, r := range normalizeRoles(meta.Roles) {
roleSet[r] = struct{}{}
}
}
roleSet[roleName] = struct{}{}
roles := make([]string, 0, len(roleSet))
for r := range roleSet {
roles = append(roles, r)
}
sort.Strings(roles)
return roles, nil
}
func mapNodeRole(role nodeconfigs.NodeRole) (string, error) {
switch role {
case nodeconfigs.NodeRoleNode:
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)
}
}
func normalizeRoles(rawRoles []string) []string {
roleSet := map[string]struct{}{}
for _, role := range rawRoles {
role = strings.ToLower(strings.TrimSpace(role))
if role != fluentBitRoleNode && role != fluentBitRoleDNS && role != fluentBitRoleHTTPDNS {
continue
}
roleSet[role] = struct{}{}
}
roles := make([]string, 0, len(roleSet))
for role := range roleSet {
roles = append(roles, role)
}
sort.Strings(roles)
return roles
}
func hasRole(roles []string, role string) bool {
for _, one := range roles {
if one == role {
return true
}
}
return false
}
func (this *BaseInstaller) buildDesiredFluentBitConfig(roles []string) (*fluentBitDesiredConfig, error) {
if len(roles) == 0 {
return nil, errors.New("fluent-bit roles should not be empty")
}
ch, err := models.SharedSysSettingDAO.ReadClickHouseConfig(nil)
if err != nil {
return nil, fmt.Errorf("read clickhouse setting failed: %w", err)
}
if ch == nil {
ch = &systemconfigs.ClickHouseSetting{}
}
if strings.TrimSpace(ch.Host) == "" {
ch.Host = "127.0.0.1"
}
ch.Scheme = strings.ToLower(strings.TrimSpace(ch.Scheme))
if ch.Scheme == "" {
ch.Scheme = "https"
}
if ch.Scheme != "http" && ch.Scheme != "https" {
return nil, fmt.Errorf("unsupported clickhouse scheme '%s'", ch.Scheme)
}
if ch.Port <= 0 {
if ch.Scheme == "https" {
ch.Port = 8443
} else {
ch.Port = 8443
}
}
if strings.TrimSpace(ch.Database) == "" {
ch.Database = "default"
}
if strings.TrimSpace(ch.User) == "" {
ch.User = "default"
}
// 当前平台策略:后台固定跳过 ClickHouse TLS 证书校验,不暴露 ServerName 配置。
ch.TLSSkipVerify = true
ch.TLSServerName = ""
httpPathPattern := fluentBitHTTPPathPattern
dnsPathPattern := fluentBitDNSPathPattern
httpdnsPathPattern := fluentBitHTTPDNSPathPattern
publicPolicyPath, err := this.readPublicAccessLogPolicyPath()
if err != nil {
return nil, err
}
policyDir := dirFromPolicyPath(publicPolicyPath)
if policyDir != "" {
pattern := strings.TrimRight(policyDir, "/") + "/*.log"
httpPathPattern = pattern
dnsPathPattern = pattern
httpdnsPathPattern = pattern
}
return &fluentBitDesiredConfig{
Roles: normalizeRoles(roles),
ClickHouse: ch,
HTTPPathPattern: httpPathPattern,
DNSPathPattern: dnsPathPattern,
HTTPDNSPathPattern: httpdnsPathPattern,
}, nil
}
func (this *BaseInstaller) readPublicAccessLogPolicyPath() (string, error) {
policyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(nil)
if err != nil {
return "", fmt.Errorf("find current public access log policy failed: %w", err)
}
if policyId <= 0 {
return "", nil
}
policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(nil, policyId)
if err != nil {
return "", fmt.Errorf("read public access log policy failed: %w", err)
}
if policy == nil {
return "", nil
}
return strings.TrimSpace(models.ParseHTTPAccessLogPolicyFilePath(policy)), nil
}
func dirFromPolicyPath(policyPath string) string {
pathValue := strings.TrimSpace(policyPath)
if pathValue == "" {
return ""
}
pathValue = strings.ReplaceAll(pathValue, "\\", "/")
dir := slashpath.Dir(pathValue)
if dir == "." {
return ""
}
return strings.TrimRight(dir, "/")
}
func (this *BaseInstaller) applyManagedConfig(tempDir string, desired *fluentBitDesiredConfig, parserContent string, existingMeta *fluentBitManagedMeta) (bool, error) {
mainExists, err := this.remoteFileExists(fluentBitMainConfigFile)
if err != nil {
return false, err
}
if mainExists && existingMeta == nil {
containsMarker, err := this.remoteFileContains(fluentBitMainConfigFile, fluentBitManagedMarker)
if err != nil {
return false, err
}
if !containsMarker {
// Adopt unmanaged config by backing it up and replacing with managed config below.
}
}
configContent, err := renderManagedConfig(desired)
if err != nil {
return false, err
}
envContent := renderManagedEnv(desired.ClickHouse)
metaContent, newMeta, err := renderManagedMeta(desired, configContent, parserContent, envContent)
if err != nil {
return false, err
}
requiredFiles := []string{fluentBitMainConfigFile, fluentBitParsersFile, fluentBitManagedEnvFile, fluentBitManagedMetaFile}
if existingMeta != nil && existingMeta.Hash == newMeta.Hash {
allExists := true
for _, file := range requiredFiles {
exists, err := this.remoteFileExists(file)
if err != nil {
return false, err
}
if !exists {
allExists = false
break
}
}
if allExists {
return false, nil
}
}
if mainExists {
backup := fluentBitMainConfigFile + ".bak." + strconv.FormatInt(time.Now().Unix(), 10)
_, stderr, err := this.client.Exec("cp -f " + shQuote(fluentBitMainConfigFile) + " " + shQuote(backup))
if err != nil {
return false, fmt.Errorf("backup existing fluent-bit config failed: %w, stderr: %s", err, stderr)
}
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitMainConfigFile, configContent, 0644); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitParsersFile, parserContent, 0644); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedEnvFile, envContent, 0600); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedMetaFile, metaContent, 0644); err != nil {
return false, err
}
return true, nil
}
func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
if desired == nil || desired.ClickHouse == nil {
return "", errors.New("invalid fluent-bit desired config")
}
scheme := strings.ToLower(strings.TrimSpace(desired.ClickHouse.Scheme))
if scheme == "" {
scheme = "http"
}
if scheme != "http" && scheme != "https" {
return "", fmt.Errorf("unsupported clickhouse scheme '%s'", desired.ClickHouse.Scheme)
}
useTLS := scheme == "https"
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,
"[SERVICE]",
" Flush 1",
" Log_Level info",
" Parsers_File " + fluentBitParsersFile,
" storage.path " + fluentBitStorageDir,
" storage.sync normal",
" storage.checksum off",
" storage.backlog.mem_limit 512MB",
"",
}
if hasRole(desired.Roles, fluentBitRoleNode) {
lines = append(lines,
"[INPUT]",
" Name tail",
" Path "+desired.HTTPPathPattern,
" Tag app.http.logs",
" Parser json",
" Refresh_Interval 2",
" Read_from_Head false",
" DB /var/lib/fluent-bit/http-logs.db",
" storage.type filesystem",
" Mem_Buf_Limit 256MB",
" Skip_Long_Lines On",
"",
)
}
if hasRole(desired.Roles, fluentBitRoleDNS) {
lines = append(lines,
"[INPUT]",
" Name tail",
" Path "+desired.DNSPathPattern,
" Tag app.dns.logs",
" Parser json",
" Refresh_Interval 2",
" Read_from_Head false",
" DB /var/lib/fluent-bit/dns-logs.db",
" storage.type filesystem",
" Mem_Buf_Limit 256MB",
" Skip_Long_Lines On",
"",
)
}
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]",
" Name http",
" Match app.http.logs",
" Host "+desired.ClickHouse.Host,
" Port "+strconv.Itoa(desired.ClickHouse.Port),
" URI /?query="+insertHTTP,
" 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, "")
}
if hasRole(desired.Roles, fluentBitRoleDNS) {
lines = append(lines,
"[OUTPUT]",
" Name http",
" Match app.dns.logs",
" Host "+desired.ClickHouse.Host,
" Port "+strconv.Itoa(desired.ClickHouse.Port),
" URI /?query="+insertDNS,
" 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, "")
}
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
}
func renderManagedEnv(ch *systemconfigs.ClickHouseSetting) string {
user := "default"
password := ""
if ch != nil {
if strings.TrimSpace(ch.User) != "" {
user = strings.TrimSpace(ch.User)
}
password = ch.Password
}
return "CH_USER=" + strconv.Quote(user) + "\n" +
"CH_PASSWORD=" + strconv.Quote(password) + "\n"
}
func renderManagedMeta(desired *fluentBitDesiredConfig, configContent string, parserContent string, envContent string) (string, *fluentBitManagedMeta, error) {
hashInput := configContent + "\n---\n" + parserContent + "\n---\n" + envContent + "\n---\n" + strings.Join(desired.Roles, ",")
hashBytes := sha256.Sum256([]byte(hashInput))
hash := fmt.Sprintf("%x", hashBytes[:])
meta := &fluentBitManagedMeta{
Roles: desired.Roles,
Hash: hash,
UpdatedAt: time.Now().Unix(),
SourceVersion: teaconst.Version,
}
data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return "", nil, fmt.Errorf("encode fluent-bit managed metadata failed: %w", err)
}
return string(data) + "\n", meta, nil
}
func (this *BaseInstaller) copyLocalFileToRemote(tempDir string, localPath string, remotePath string, mode os.FileMode) error {
tempFile := tempDir + "/" + filepath.Base(remotePath)
if err := this.client.Copy(localPath, tempFile, mode); err != nil {
return fmt.Errorf("upload fluent-bit file '%s' failed: %w", localPath, err)
}
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
if err != nil {
return fmt.Errorf("install remote fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
}
return nil
}
func (this *BaseInstaller) writeRemoteFileByTemp(tempDir string, remotePath string, content string, mode os.FileMode) error {
tempFile := tempDir + "/" + filepath.Base(remotePath) + ".tmp"
if _, err := this.client.WriteFile(tempFile, []byte(content)); err != nil {
return fmt.Errorf("write temp fluent-bit file '%s' failed: %w", tempFile, err)
}
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
if err != nil {
return fmt.Errorf("write managed fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
}
return nil
}
func (this *BaseInstaller) ensureFluentBitService(tempDir string, binPath string, configChanged bool) error {
_, _, _ = this.client.Exec("if command -v systemctl >/dev/null 2>&1 && [ ! -f /etc/systemd/system/" + fluentBitServiceName + ".service ] && [ ! -f /lib/systemd/system/" + fluentBitServiceName + ".service ]; then " +
"cat > /etc/systemd/system/" + fluentBitServiceName + ".service <<'EOF'\n" +
"[Unit]\n" +
"Description=Fluent Bit\n" +
"After=network.target\n" +
"\n" +
"[Service]\n" +
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n" +
"Restart=always\n" +
"RestartSec=5\n" +
"\n" +
"[Install]\n" +
"WantedBy=multi-user.target\n" +
"EOF\n" +
"fi")
stdout, _, err := this.client.Exec("if command -v systemctl >/dev/null 2>&1; then echo 1; else echo 0; fi")
if err != nil {
return fmt.Errorf("check systemctl failed: %w", err)
}
if strings.TrimSpace(stdout) == "1" {
dropInChanged, err := this.ensureServiceDropIn(tempDir, binPath)
if err != nil {
return err
}
restartRequired := configChanged || dropInChanged
_, stderr, err := this.client.Exec("systemctl daemon-reload; systemctl enable " + fluentBitServiceName + " >/dev/null 2>&1 || true; " +
"if systemctl is-active " + fluentBitServiceName + " >/dev/null 2>&1; then " +
"if [ \"" + boolToString(restartRequired) + "\" = \"1\" ]; then systemctl restart " + fluentBitServiceName + "; fi; " +
"else systemctl start " + fluentBitServiceName + "; fi")
if err != nil {
return fmt.Errorf("ensure fluent-bit service failed: %w, stderr: %s", err, stderr)
}
return nil
}
if configChanged {
_, _, _ = this.client.Exec("pkill -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1 || true")
}
_, _, runningErr := this.client.Exec("pgrep -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1")
if runningErr != nil {
startCmd := "set -a; [ -f " + shQuote(fluentBitManagedEnvFile) + " ] && . " + shQuote(fluentBitManagedEnvFile) + "; set +a; " +
shQuote(binPath) + " -c " + shQuote(fluentBitMainConfigFile) + " >/dev/null 2>&1 &"
_, stderr, err := this.client.Exec(startCmd)
if err != nil {
return fmt.Errorf("start fluent-bit without systemd failed: %w, stderr: %s", err, stderr)
}
}
return nil
}
func (this *BaseInstaller) ensureServiceDropIn(tempDir string, binPath string) (bool, error) {
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitDropInDir))
if err != nil {
return false, fmt.Errorf("prepare fluent-bit drop-in dir failed: %w, stderr: %s", err, stderr)
}
content := "[Service]\n" +
"EnvironmentFile=-" + fluentBitManagedEnvFile + "\n" +
"ExecStart=\n" +
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n"
existing, _, _ := this.client.Exec("if [ -f " + shQuote(fluentBitDropInFile) + " ]; then cat " + shQuote(fluentBitDropInFile) + "; fi")
if existing == content {
return false, nil
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitDropInFile, content, 0644); err != nil {
return false, err
}
return true, nil
}
func (this *BaseInstaller) remoteFileExists(path string) (bool, error) {
stdout, stderr, err := this.client.Exec("if [ -f " + shQuote(path) + " ]; then echo 1; else echo 0; fi")
if err != nil {
return false, fmt.Errorf("check remote file '%s' failed: %w, stderr: %s", path, err, stderr)
}
return strings.TrimSpace(stdout) == "1", nil
}
func (this *BaseInstaller) remoteFileContains(path string, pattern string) (bool, error) {
stdout, stderr, err := this.client.Exec("if grep -F " + shQuote(pattern) + " " + shQuote(path) + " >/dev/null 2>&1; then echo 1; else echo 0; fi")
if err != nil {
return false, fmt.Errorf("check remote file content '%s' failed: %w, stderr: %s", path, err, stderr)
}
return strings.TrimSpace(stdout) == "1", nil
}
func shQuote(value string) string {
if value == "" {
return "''"
}
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
func boolToString(v bool) string {
if v {
return "1"
}
return "0"
}

View File

@@ -279,3 +279,27 @@ func (this *BaseInstaller) uname() (uname string) {
return "x86_64 GNU/Linux" return "x86_64 GNU/Linux"
} }
func parseOSRelease(content string) map[string]string {
result := map[string]string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
continue
}
parts := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, "\"")
result[key] = value
}
return result
}
func shQuote(s string) string {
if len(s) == 0 {
return "''"
}
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
}

View File

@@ -131,11 +131,6 @@ https.key: "${keyFile}"`)
return fmt.Errorf("write '%s': %w", configFile, err) 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 " startCmdPrefix := "cd " + shQuote(appDir+"/configs") + " && ../bin/edge-httpdns "

View File

@@ -137,12 +137,6 @@ secret: "${nodeSecret}"`)
} }
} }
// 在线安装/更新 Fluent Bit与边缘节点安装流程联动
err = this.SetupFluentBit(nodeconfigs.NodeRoleNode)
if err != nil {
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
return fmt.Errorf("setup fluent-bit failed: %w", err)
}
// 测试 // 测试
_, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node test") _, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node test")

View File

@@ -139,12 +139,6 @@ secret: "${nodeSecret}"`)
} }
} }
// 在线安装/更新 Fluent Bit与 DNS 节点安装流程联动)
err = this.SetupFluentBit(nodeconfigs.NodeRoleDNS)
if err != nil {
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
return fmt.Errorf("setup fluent-bit failed: %w", err)
}
// 测试 // 测试
_, stderr, err = this.client.Exec(dir + "/edge-dns/bin/edge-dns test") _, stderr, err = this.client.Exec(dir + "/edge-dns/bin/edge-dns test")

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/accounts" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/accounts"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/anti-ddos" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/anti-ddos"
httpdnsServices "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/httpdns"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/nameservers" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/nameservers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/posts" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/posts"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/reporters" "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/reporters"
@@ -313,4 +314,9 @@ func APINodeServicesRegister(node *APINode, server *grpc.Server) {
pb.RegisterPostCategoryServiceServer(server, instance) pb.RegisterPostCategoryServiceServer(server, instance)
node.rest(instance) node.rest(instance)
} }
{
var instance = node.serviceInstance(&httpdnsServices.HTTPDNSBoardService{}).(*httpdnsServices.HTTPDNSBoardService)
pb.RegisterHTTPDNSBoardServiceServer(server, instance)
node.rest(instance)
}
} }

View File

@@ -0,0 +1,324 @@
package httpdns
import (
"context"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
timeutil "github.com/iwind/TeaGo/utils/time"
)
// HTTPDNSBoardService HTTPDNS 仪表盘服务
type HTTPDNSBoardService struct {
services.BaseService
}
// ComposeHTTPDNSBoard 组合看板数据
func (this *HTTPDNSBoardService) ComposeHTTPDNSBoard(ctx context.Context, req *pb.ComposeHTTPDNSBoardRequest) (*pb.ComposeHTTPDNSBoardResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
tx := this.NullTx()
result := &pb.ComposeHTTPDNSBoardResponse{}
countApps, err := models.SharedHTTPDNSAppDAO.CountEnabledApps(tx, "")
if err != nil {
return nil, err
}
result.CountApps = countApps
countDomains, err := models.SharedHTTPDNSDomainDAO.CountEnabledDomains(tx, 0)
if err != nil {
return nil, err
}
result.CountDomains = countDomains
countClusters, err := models.SharedHTTPDNSClusterDAO.CountEnabledClusters(tx, "")
if err != nil {
return nil, err
}
result.CountClusters = countClusters
allNodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(tx, 0)
if err != nil {
return nil, err
}
result.CountNodes = int64(len(allNodes))
var countOffline int64
for _, node := range allNodes {
if !node.IsActive {
countOffline++
}
}
result.CountOfflineNodes = countOffline
hourFrom := timeutil.Format("YmdH", time.Now().Add(-23*time.Hour))
hourTo := timeutil.Format("YmdH")
dayFrom := timeutil.Format("Ymd", time.Now().AddDate(0, 0, -14))
dayTo := timeutil.Format("Ymd")
todayFrom := timeutil.Format("Ymd", time.Now().Add(-24*time.Hour))
store := clickhouse.NewHTTPDNSAccessLogsStore()
if store.Client().IsConfigured() {
err = this.composeTrafficAndRanksFromClickHouse(ctx, tx, store, result, hourFrom, hourTo, dayFrom, dayTo, todayFrom)
}
if err != nil || !store.Client().IsConfigured() {
err = this.composeTrafficAndRanksFromMySQL(tx, result, hourFrom, hourTo, dayFrom, dayTo, todayFrom)
if err != nil {
return nil, err
}
}
err = this.fillNodeValues(tx, result)
if err != nil {
return nil, err
}
return result, nil
}
func (this *HTTPDNSBoardService) composeTrafficAndRanksFromClickHouse(ctx context.Context, tx *dbs.Tx, store *clickhouse.HTTPDNSAccessLogsStore, result *pb.ComposeHTTPDNSBoardResponse, hourFrom string, hourTo string, dayFrom string, dayTo string, todayFrom string) error {
hourlyStats, err := store.FindHourlyStats(ctx, hourFrom, hourTo)
if err != nil {
return err
}
hourlyMap := map[string]*clickhouse.HTTPDNSAccessLogHourlyStat{}
for _, stat := range hourlyStats {
hourlyMap[stat.Hour] = stat
}
hours, err := utils.RangeHours(hourFrom, hourTo)
if err != nil {
return err
}
for _, hour := range hours {
stat, ok := hourlyMap[hour]
if ok {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{
Hour: hour,
CountRequests: stat.CountRequests,
})
} else {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{Hour: hour})
}
}
dailyStats, err := store.FindDailyStats(ctx, dayFrom, dayTo)
if err != nil {
return err
}
dailyMap := map[string]*clickhouse.HTTPDNSAccessLogDailyStat{}
for _, stat := range dailyStats {
dailyMap[stat.Day] = stat
}
days, err := utils.RangeDays(dayFrom, dayTo)
if err != nil {
return err
}
for _, day := range days {
stat, ok := dailyMap[day]
if ok {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{
Day: day,
CountRequests: stat.CountRequests,
})
} else {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{Day: day})
}
}
topAppStats, err := store.ListTopApps(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topAppStats {
appName := stat.AppName
if len(appName) == 0 {
appName = stat.AppId
}
result.TopAppStats = append(result.TopAppStats, &pb.ComposeHTTPDNSBoardResponse_TopAppStat{
AppId: 0,
AppName: appName,
CountRequests: stat.CountRequests,
})
}
topDomainStats, err := store.ListTopDomains(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topDomainStats {
result.TopDomainStats = append(result.TopDomainStats, &pb.ComposeHTTPDNSBoardResponse_TopDomainStat{
DomainName: stat.Domain,
CountRequests: stat.CountRequests,
})
}
topNodeStats, err := store.ListTopNodes(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topNodeStats {
nodeName := ""
node, nodeErr := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, int64(stat.NodeId))
if nodeErr == nil && node != nil {
nodeName = node.Name
}
if len(nodeName) == 0 {
continue
}
result.TopNodeStats = append(result.TopNodeStats, &pb.ComposeHTTPDNSBoardResponse_TopNodeStat{
ClusterId: int64(stat.ClusterId),
NodeId: int64(stat.NodeId),
NodeName: nodeName,
CountRequests: stat.CountRequests,
})
}
return nil
}
func (this *HTTPDNSBoardService) composeTrafficAndRanksFromMySQL(tx *dbs.Tx, result *pb.ComposeHTTPDNSBoardResponse, hourFrom string, hourTo string, dayFrom string, dayTo string, todayFrom string) error {
hourlyStats, err := models.SharedHTTPDNSAccessLogDAO.FindHourlyStats(tx, hourFrom, hourTo)
if err != nil {
return err
}
hourlyMap := map[string]*models.HTTPDNSAccessLogHourlyStat{}
for _, stat := range hourlyStats {
hourlyMap[stat.Hour] = stat
}
hours, err := utils.RangeHours(hourFrom, hourTo)
if err != nil {
return err
}
for _, hour := range hours {
stat, ok := hourlyMap[hour]
if ok {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{
Hour: hour,
CountRequests: stat.CountRequests,
})
} else {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{Hour: hour})
}
}
dailyStats, err := models.SharedHTTPDNSAccessLogDAO.FindDailyStats(tx, dayFrom, dayTo)
if err != nil {
return err
}
dailyMap := map[string]*models.HTTPDNSAccessLogDailyStat{}
for _, stat := range dailyStats {
dailyMap[stat.Day] = stat
}
days, err := utils.RangeDays(dayFrom, dayTo)
if err != nil {
return err
}
for _, day := range days {
stat, ok := dailyMap[day]
if ok {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{
Day: day,
CountRequests: stat.CountRequests,
})
} else {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{Day: day})
}
}
topAppStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopApps(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topAppStats {
appName := stat.AppName
if len(appName) == 0 {
appName = stat.AppId
}
result.TopAppStats = append(result.TopAppStats, &pb.ComposeHTTPDNSBoardResponse_TopAppStat{
AppId: 0,
AppName: appName,
CountRequests: stat.CountRequests,
})
}
topDomainStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopDomains(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topDomainStats {
result.TopDomainStats = append(result.TopDomainStats, &pb.ComposeHTTPDNSBoardResponse_TopDomainStat{
DomainName: stat.Domain,
CountRequests: stat.CountRequests,
})
}
topNodeStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopNodes(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topNodeStats {
nodeName := ""
node, nodeErr := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, int64(stat.NodeId))
if nodeErr == nil && node != nil {
nodeName = node.Name
}
if len(nodeName) == 0 {
continue
}
result.TopNodeStats = append(result.TopNodeStats, &pb.ComposeHTTPDNSBoardResponse_TopNodeStat{
ClusterId: int64(stat.ClusterId),
NodeId: int64(stat.NodeId),
NodeName: nodeName,
CountRequests: stat.CountRequests,
})
}
return nil
}
func (this *HTTPDNSBoardService) fillNodeValues(tx *dbs.Tx, result *pb.ComposeHTTPDNSBoardResponse) error {
cpuValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemCPU, "usage", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range cpuValues {
result.CpuNodeValues = append(result.CpuNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
memoryValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemMemory, "usage", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range memoryValues {
result.MemoryNodeValues = append(result.MemoryNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
loadValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemLoad, "load1m", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range loadValues {
result.LoadNodeValues = append(result.LoadNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
return nil
}

View File

@@ -17,7 +17,7 @@ type NodeValueService struct {
// CreateNodeValue 记录数据 // CreateNodeValue 记录数据
func (this *NodeValueService) CreateNodeValue(ctx context.Context, req *pb.CreateNodeValueRequest) (*pb.RPCSuccess, error) { func (this *NodeValueService) CreateNodeValue(ctx context.Context, req *pb.CreateNodeValueRequest) (*pb.RPCSuccess, error) {
role, nodeId, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeUser) role, nodeId, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeUser, rpcutils.UserTypeHTTPDNS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -30,6 +30,8 @@ func (this *NodeValueService) CreateNodeValue(ctx context.Context, req *pb.Creat
clusterId, err = models.SharedNodeDAO.FindNodeClusterId(tx, nodeId) clusterId, err = models.SharedNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeDNS: case rpcutils.UserTypeDNS:
clusterId, err = models.SharedNSNodeDAO.FindNodeClusterId(tx, nodeId) clusterId, err = models.SharedNSNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeHTTPDNS:
clusterId, err = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeUser: case rpcutils.UserTypeUser:
} }
if err != nil { if err != nil {

View File

@@ -97,12 +97,10 @@ function build() {
# create dir & copy files # create dir & copy files
echo "copying ..." echo "copying ..."
if [ ! -d "$DIST" ]; then rm -rf "$DIST"
mkdir "$DIST" mkdir -p "$DIST"/bin
mkdir "$DIST"/bin mkdir -p "$DIST"/configs
mkdir "$DIST"/configs mkdir -p "$DIST"/logs
mkdir "$DIST"/logs
fi
cp -R "$ROOT"/../web "$DIST"/ cp -R "$ROOT"/../web "$DIST"/
rm -f "$DIST"/web/tmp/* rm -f "$DIST"/web/tmp/*
@@ -135,30 +133,6 @@ function build() {
rm -f "$(basename "$EDGE_API_ZIP_FILE")" rm -f "$(basename "$EDGE_API_ZIP_FILE")"
# ensure edge-api package always contains fluent-bit runtime assets/packages # ensure edge-api package always contains fluent-bit runtime assets/packages
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/edge-api/deploy/fluent-bit"
if [ -d "$FLUENT_ROOT" ]; then
verify_fluent_bit_package_matrix "$FLUENT_ROOT" "$ARCH" || exit 1
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
FLUENT_FILES=(
"parsers.conf"
)
for file in "${FLUENT_FILES[@]}"; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ -d "$FLUENT_ROOT/packages" ]; then
cp -R "$FLUENT_ROOT/packages" "$FLUENT_DIST/"
fi
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
fi
# 鍒犻櫎 MaxMind 鏁版嵁搴撴枃浠讹紙浣跨敤宓屽叆鐨勬暟鎹簱锛屼笉闇€瑕佸閮ㄦ枃浠讹級 # 鍒犻櫎 MaxMind 鏁版嵁搴撴枃浠讹紙浣跨敤宓屽叆鐨勬暟鎹簱锛屼笉闇€瑕佸閮ㄦ枃浠讹級
find . -name "*.mmdb" -type f -delete find . -name "*.mmdb" -type f -delete
@@ -220,39 +194,6 @@ function build() {
echo "[done]" echo "[done]"
} }
function verify_fluent_bit_package_matrix() {
FLUENT_ROOT=$1
ARCH=$2
REQUIRED_FILES=()
if [ "$ARCH" = "amd64" ]; then
REQUIRED_FILES=(
"packages/linux-amd64/fluent-bit_4.2.2_amd64.deb"
"packages/linux-amd64/fluent-bit-4.2.2-1.x86_64.rpm"
)
elif [ "$ARCH" = "arm64" ]; then
REQUIRED_FILES=(
"packages/linux-arm64/fluent-bit_4.2.2_arm64.deb"
"packages/linux-arm64/fluent-bit-4.2.2-1.aarch64.rpm"
)
else
echo "[error] unsupported arch for fluent-bit package validation: $ARCH"
return 1
fi
MISSING=0
for FILE in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$FLUENT_ROOT/$FILE" ]; then
echo "[error] fluent-bit matrix package missing: $FLUENT_ROOT/$FILE"
MISSING=1
fi
done
if [ "$MISSING" -ne 0 ]; then
return 1
fi
return 0
}
function lookup-version() { function lookup-version() {
FILE=$1 FILE=$1
VERSION_DATA=$(cat "$FILE") VERSION_DATA=$(cat "$FILE")

View File

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

View File

@@ -248,3 +248,7 @@ func (this *RPCClient) PostCategoryRPC() pb.PostCategoryServiceClient {
func (this *RPCClient) PostRPC() pb.PostServiceClient { func (this *RPCClient) PostRPC() pb.PostServiceClient {
return pb.NewPostServiceClient(this.pickConn()) return pb.NewPostServiceClient(this.pickConn())
} }
func (this *RPCClient) HTTPDNSBoardRPC() pb.HTTPDNSBoardServiceClient {
return pb.NewHTTPDNSBoardServiceClient(this.pickConn())
}

View File

@@ -11,7 +11,6 @@ import (
nodethresholds "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/thresholds" nodethresholds "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/thresholds"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cc" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/http3" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/http3"
networksecurity "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/network-security"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/pages" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/pages"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/thresholds" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/thresholds"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/uam" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/uam"
@@ -53,7 +52,6 @@ func init() {
GetPost("/thresholds", new(thresholds.IndexAction)). GetPost("/thresholds", new(thresholds.IndexAction)).
// //
GetPost("/network-security", new(networksecurity.IndexAction)).
// 节点设置相关 // 节点设置相关
Prefix("/clusters/cluster/node/settings"). Prefix("/clusters/cluster/node/settings").

View File

@@ -1,95 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package networksecurity
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "index")
this.SecondMenu("networkSecurity")
}
func (this *IndexAction) RunGet(params struct {
ClusterId int64
}) {
policyResp, err := this.RPC().NodeClusterRPC().FindNodeClusterNetworkSecurityPolicy(this.AdminContext(), &pb.FindNodeClusterNetworkSecurityPolicyRequest{
NodeClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
}
var policy = nodeconfigs.NewNetworkSecurityPolicy()
if len(policyResp.NetworkSecurityPolicyJSON) > 0 {
err = json.Unmarshal(policyResp.NetworkSecurityPolicyJSON, policy)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["policy"] = policy
this.Show()
}
func (this *IndexAction) RunPost(params struct {
ClusterId int64
Status string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
policyResp, err := this.RPC().NodeClusterRPC().FindNodeClusterNetworkSecurityPolicy(this.AdminContext(), &pb.FindNodeClusterNetworkSecurityPolicyRequest{
NodeClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
}
var policy = nodeconfigs.NewNetworkSecurityPolicy()
if len(policyResp.NetworkSecurityPolicyJSON) > 0 {
err = json.Unmarshal(policyResp.NetworkSecurityPolicyJSON, policy)
if err != nil {
this.ErrorPage(err)
return
}
}
policy.Status = params.Status
err = policy.Init()
if err != nil {
this.Fail("配置校验失败:" + err.Error())
return
}
policyJSON, err := json.Marshal(policy)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().NodeClusterRPC().UpdateNodeClusterNetworkSecurityPolicy(this.AdminContext(), &pb.UpdateNodeClusterNetworkSecurityPolicyRequest{
NodeClusterId: params.ClusterId,
NetworkSecurityPolicyJSON: policyJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -44,17 +44,10 @@ func (this *ClusterHelper) filterMenuItems1(items []maps.Map, info *pb.FindEnabl
func (this *ClusterHelper) filterMenuItems2(items []maps.Map, info *pb.FindEnabledNodeClusterConfigInfoResponse, clusterIdString string, selectedItem string, actionPtr actions.ActionWrapper) []maps.Map { func (this *ClusterHelper) filterMenuItems2(items []maps.Map, info *pb.FindEnabledNodeClusterConfigInfoResponse, clusterIdString string, selectedItem string, actionPtr actions.ActionWrapper) []maps.Map {
if teaconst.IsPlus { if teaconst.IsPlus {
items = append(items, maps.Map{
"name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingSecurityPolicy),
"url": "/clusters/cluster/settings/network-security?clusterId=" + clusterIdString,
"isActive": selectedItem == "networkSecurity",
"isOn": info != nil && info.HasNetworkSecurityPolicy, // TODO 将来 加入 info.HasDDoSProtection
})
items = append(items, maps.Map{
"name": "-",
})
if plusutils.CheckComponent(plusutils.ComponentCodeScheduling) { if plusutils.CheckComponent(plusutils.ComponentCodeScheduling) {
items = append(items, maps.Map{
"name": "-",
})
items = append(items, maps.Map{ items = append(items, maps.Map{
"name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingSchedule), "name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingSchedule),
"url": "/clusters/cluster/settings/schedule?clusterId=" + clusterIdString, "url": "/clusters/cluster/settings/schedule?clusterId=" + clusterIdString,
@@ -89,14 +82,12 @@ func (this *ClusterHelper) filterMenuItems2(items []maps.Map, info *pb.FindEnabl
"isOn": info != nil && info.HasSystemServices, "isOn": info != nil && info.HasSystemServices,
}) })
{ items = append(items, maps.Map{
items = append(items, maps.Map{ "name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingTOA),
"name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingTOA), "url": "/clusters/cluster/settings/toa?clusterId=" + clusterIdString,
"url": "/clusters/cluster/settings/toa?clusterId=" + clusterIdString, "isActive": selectedItem == "toa",
"isActive": selectedItem == "toa", "isOn": info != nil && info.IsTOAEnabled,
"isOn": info != nil && info.IsTOAEnabled, })
})
}
} }
return items return items
} }

View File

@@ -0,0 +1,127 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package boards
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard/boards/boardutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type HTTPDNSAction struct {
actionutils.ParentAction
}
func (this *HTTPDNSAction) Init() {
this.Nav("", "", "httpdns")
this.ViewDir("@default")
this.View("dashboard/boards/httpdns")
}
func (this *HTTPDNSAction) RunGet(params struct{}) {
if !teaconst.IsPlus {
this.RedirectURL("/dashboard")
return
}
err := boardutils.InitBoard(this.Parent())
if err != nil {
this.ErrorPage(err)
return
}
this.Data["board"] = maps.Map{
"countApps": 0,
"countDomains": 0,
"countClusters": 0,
"countNodes": 0,
"countOfflineNodes": 0,
}
this.Show()
}
func (this *HTTPDNSAction) RunPost(params struct{}) {
resp, err := this.RPC().HTTPDNSBoardRPC().ComposeHTTPDNSBoard(this.AdminContext(), &pb.ComposeHTTPDNSBoardRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["board"] = maps.Map{
"countApps": resp.CountApps,
"countDomains": resp.CountDomains,
"countClusters": resp.CountClusters,
"countNodes": resp.CountNodes,
"countOfflineNodes": resp.CountOfflineNodes,
}
{
var statMaps = []maps.Map{}
for _, stat := range resp.HourlyTrafficStats {
statMaps = append(statMaps, maps.Map{
"day": stat.Hour[4:6] + "月" + stat.Hour[6:8] + "日",
"hour": stat.Hour[8:],
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["hourlyStats"] = statMaps
}
{
var statMaps = []maps.Map{}
for _, stat := range resp.DailyTrafficStats {
statMaps = append(statMaps, maps.Map{
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["dailyStats"] = statMaps
}
{
var statMaps = []maps.Map{}
for _, stat := range resp.TopAppStats {
statMaps = append(statMaps, maps.Map{
"appId": stat.AppId,
"appName": stat.AppName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topAppStats"] = statMaps
}
{
var statMaps = []maps.Map{}
for _, stat := range resp.TopDomainStats {
statMaps = append(statMaps, maps.Map{
"domainId": stat.DomainId,
"domainName": stat.DomainName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topDomainStats"] = statMaps
}
{
var statMaps = []maps.Map{}
for _, stat := range resp.TopNodeStats {
statMaps = append(statMaps, maps.Map{
"clusterId": stat.ClusterId,
"nodeId": stat.NodeId,
"nodeName": stat.NodeName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topNodeStats"] = statMaps
}
this.Success()
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time" timeutil "github.com/iwind/TeaGo/utils/time"
"regexp" "regexp"
"time" "time"
@@ -409,5 +410,55 @@ func (this *IndexAction) RunPost(params struct {
} }
this.Data["countWeakAdmins"] = countWeakAdminsResp.Count this.Data["countWeakAdmins"] = countWeakAdminsResp.Count
upgradeConfig, err := configloaders.LoadUpgradeConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["autoUpgrade"] = upgradeConfig.AutoUpgrade
httpdnsNodeUpgradeInfo, err := this.composeHTTPDNSNodeUpgradeInfo()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["httpdnsNodeUpgradeInfo"] = httpdnsNodeUpgradeInfo
this.Success() this.Success()
} }
func (this *IndexAction) composeHTTPDNSNodeUpgradeInfo() (maps.Map, error) {
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return nil, err
}
count := 0
version := ""
for _, cluster := range clustersResp.Clusters {
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: cluster.Id,
})
if err != nil {
return nil, err
}
count += len(resp.Nodes)
for _, nodeUpgrade := range resp.Nodes {
if len(nodeUpgrade.NewVersion) == 0 {
continue
}
if len(version) == 0 || stringutil.VersionCompare(version, nodeUpgrade.NewVersion) < 0 {
version = nodeUpgrade.NewVersion
}
}
}
return maps.Map{
"count": count,
"version": version,
}, nil
}

View File

@@ -245,5 +245,12 @@ func (this *IndexAction) RunPost(params struct{}) {
} }
this.Data["countWeakAdmins"] = countWeakAdminsResp.Count this.Data["countWeakAdmins"] = countWeakAdminsResp.Count
upgradeConfig, err := configloaders.LoadUpgradeConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["autoUpgrade"] = upgradeConfig.AutoUpgrade
this.Success() this.Success()
} }

View File

@@ -22,6 +22,7 @@ func init() {
Get("/waf", new(boards.WafAction)). Get("/waf", new(boards.WafAction)).
Post("/wafLogs", new(boards.WafLogsAction)). Post("/wafLogs", new(boards.WafLogsAction)).
GetPost("/dns", new(boards.DnsAction)). GetPost("/dns", new(boards.DnsAction)).
GetPost("/httpdns", new(boards.HTTPDNSAction)).
Get("/user", new(boards.UserAction)). Get("/user", new(boards.UserAction)).
Get("/events", new(boards.EventsAction)). Get("/events", new(boards.EventsAction)).
Post("/readLogs", new(boards.ReadLogsAction)). Post("/readLogs", new(boards.ReadLogsAction)).

View File

@@ -1,11 +1,110 @@
package httpdns package httpdns
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct { type IndexAction struct {
actionutils.ParentAction actionutils.ParentAction
} }
func (this *IndexAction) RunGet(params struct{}) { func (this *IndexAction) Init() {
this.RedirectURL("/httpdns/clusters") this.Nav("", "", "httpdns")
}
func (this *IndexAction) RunGet(params struct{}) {
this.Data["board"] = maps.Map{
"countApps": 0,
"countDomains": 0,
"countClusters": 0,
"countNodes": 0,
"countOfflineNodes": 0,
}
this.Show()
}
func (this *IndexAction) RunPost(params struct{}) {
resp, err := this.RPC().HTTPDNSBoardRPC().ComposeHTTPDNSBoard(this.AdminContext(), &pb.ComposeHTTPDNSBoardRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["board"] = maps.Map{
"countApps": resp.CountApps,
"countDomains": resp.CountDomains,
"countClusters": resp.CountClusters,
"countNodes": resp.CountNodes,
"countOfflineNodes": resp.CountOfflineNodes,
}
{
statMaps := []maps.Map{}
for _, stat := range resp.HourlyTrafficStats {
statMaps = append(statMaps, maps.Map{
"day": stat.Hour[4:6] + "月" + stat.Hour[6:8] + "日",
"hour": stat.Hour[8:],
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["hourlyStats"] = statMaps
}
{
statMaps := []maps.Map{}
for _, stat := range resp.DailyTrafficStats {
statMaps = append(statMaps, maps.Map{
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["dailyStats"] = statMaps
}
{
statMaps := []maps.Map{}
for _, stat := range resp.TopAppStats {
statMaps = append(statMaps, maps.Map{
"appId": stat.AppId,
"appName": stat.AppName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topAppStats"] = statMaps
}
{
statMaps := []maps.Map{}
for _, stat := range resp.TopDomainStats {
statMaps = append(statMaps, maps.Map{
"domainId": stat.DomainId,
"domainName": stat.DomainName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topDomainStats"] = statMaps
}
{
statMaps := []maps.Map{}
for _, stat := range resp.TopNodeStats {
statMaps = append(statMaps, maps.Map{
"clusterId": stat.ClusterId,
"nodeId": stat.NodeId,
"nodeName": stat.NodeName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topNodeStats"] = statMaps
}
this.Success()
} }

View File

@@ -11,9 +11,9 @@ func init() {
server. server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
Data("teaMenu", "httpdns"). Data("teaMenu", "httpdns").
Data("teaSubMenu", "cluster"). Data("teaSubMenu", "board").
Prefix("/httpdns"). Prefix("/httpdns").
Get("", new(IndexAction)). GetPost("", new(IndexAction)).
GetPost("/addPortPopup", new(AddPortPopupAction)). GetPost("/addPortPopup", new(AddPortPopupAction)).
EndAll() EndAll()
}) })

View File

@@ -25,7 +25,7 @@ func (this *CreatePopupAction) Init() {
} }
func (this *CreatePopupAction) RunGet(params struct{}) { func (this *CreatePopupAction) RunGet(params struct{}) {
var authMethods = []*serverconfigs.HTTPAuthTypeDefinition{} authMethods := []*serverconfigs.HTTPAuthTypeDefinition{}
for _, method := range serverconfigs.FindAllHTTPAuthTypes(teaconst.Role) { for _, method := range serverconfigs.FindAllHTTPAuthTypes(teaconst.Role) {
if !method.IsPlus || (method.IsPlus && teaconst.IsPlus) { if !method.IsPlus || (method.IsPlus && teaconst.IsPlus) {
authMethods = append(authMethods, method) authMethods = append(authMethods, method)
@@ -59,6 +59,10 @@ func (this *CreatePopupAction) RunPost(params struct {
TypeDTimestampParamName string TypeDTimestampParamName string
TypeDLife int TypeDLife int
// TypeE
TypeESecret string
TypeELife int
// BasicAuth // BasicAuth
HttpAuthBasicAuthUsersJSON []byte HttpAuthBasicAuthUsersJSON []byte
BasicAuthRealm string BasicAuthRealm string
@@ -81,29 +85,25 @@ func (this *CreatePopupAction) RunPost(params struct {
Field("type", params.Type). Field("type", params.Type).
Require("请输入鉴权类型") Require("请输入鉴权类型")
var ref = &serverconfigs.HTTPAuthPolicyRef{IsOn: true} ref := &serverconfigs.HTTPAuthPolicyRef{IsOn: true}
var method serverconfigs.HTTPAuthMethodInterface var method serverconfigs.HTTPAuthMethodInterface
// 扩展名 exts := utils.NewStringsStream(params.Exts).
var exts = utils.NewStringsStream(params.Exts).
Map(strings.TrimSpace, strings.ToLower). Map(strings.TrimSpace, strings.ToLower).
Filter(utils.FilterNotEmpty). Filter(utils.FilterNotEmpty).
Map(utils.MapAddPrefixFunc(".")). Map(utils.MapAddPrefixFunc(".")).
Unique(). Unique().
Result() Result()
// 域名 domains := []string{}
var domains = []string{}
if len(params.DomainsJSON) > 0 { if len(params.DomainsJSON) > 0 {
var rawDomains = []string{} rawDomains := []string{}
err := json.Unmarshal(params.DomainsJSON, &rawDomains) err := json.Unmarshal(params.DomainsJSON, &rawDomains)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
// TODO 如果用户填写了一个网址,应该分析域名并填入
domains = utils.NewStringsStream(rawDomains). domains = utils.NewStringsStream(rawDomains).
Map(strings.TrimSpace, strings.ToLower). Map(strings.TrimSpace, strings.ToLower).
Filter(utils.FilterNotEmpty). Filter(utils.FilterNotEmpty).
@@ -116,11 +116,11 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeASecret", params.TypeASecret). Field("typeASecret", params.TypeASecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字"). Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字").
Field("typeASignParamName", params.TypeASignParamName). Field("typeASignParamName", params.TypeASignParamName).
Require("请输入签名参数"). Require("请输入签名参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线") Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线")
if params.TypeALife < 0 { if params.TypeALife < 0 {
params.TypeALife = 0 params.TypeALife = 0
@@ -135,8 +135,8 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeBSecret", params.TypeBSecret). Field("typeBSecret", params.TypeBSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字") Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字")
method = &serverconfigs.HTTPAuthTypeBMethod{ method = &serverconfigs.HTTPAuthTypeBMethod{
Secret: params.TypeBSecret, Secret: params.TypeBSecret,
@@ -146,8 +146,8 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeCSecret", params.TypeCSecret). Field("typeCSecret", params.TypeCSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字") Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字")
method = &serverconfigs.HTTPAuthTypeCMethod{ method = &serverconfigs.HTTPAuthTypeCMethod{
Secret: params.TypeCSecret, Secret: params.TypeCSecret,
@@ -157,14 +157,14 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeDSecret", params.TypeDSecret). Field("typeDSecret", params.TypeDSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字"). Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字").
Field("typeDSignParamName", params.TypeDSignParamName). Field("typeDSignParamName", params.TypeDSignParamName).
Require("请输入签名参数"). Require("请输入签名参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线"). Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线").
Field("typeDTimestampParamName", params.TypeDTimestampParamName). Field("typeDTimestampParamName", params.TypeDTimestampParamName).
Require("请输入时间参数"). Require("请输入时间参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "时间参数中只能包含字母、数字下划线") Match(`^[a-zA-Z0-9_]{1,40}$`, "时间参数中只能包含字母、数字下划线")
method = &serverconfigs.HTTPAuthTypeDMethod{ method = &serverconfigs.HTTPAuthTypeDMethod{
Secret: params.TypeDSecret, Secret: params.TypeDSecret,
@@ -172,15 +172,26 @@ func (this *CreatePopupAction) RunPost(params struct {
TimestampParamName: params.TypeDTimestampParamName, TimestampParamName: params.TypeDTimestampParamName,
Life: params.TypeDLife, Life: params.TypeDLife,
} }
case serverconfigs.HTTPAuthTypeTypeE:
params.Must.
Field("typeESecret", params.TypeESecret).
Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母和数字")
method = &serverconfigs.HTTPAuthTypeEMethod{
Secret: params.TypeESecret,
Life: params.TypeELife,
}
case serverconfigs.HTTPAuthTypeBasicAuth: case serverconfigs.HTTPAuthTypeBasicAuth:
var users = []*serverconfigs.HTTPAuthBasicMethodUser{} users := []*serverconfigs.HTTPAuthBasicMethodUser{}
err := json.Unmarshal(params.HttpAuthBasicAuthUsersJSON, &users) err := json.Unmarshal(params.HttpAuthBasicAuthUsersJSON, &users)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
if len(users) == 0 { if len(users) == 0 {
this.Fail("请添加至少一个用户") this.Fail("请至少添加一个用户")
} }
method = &serverconfigs.HTTPAuthBasicMethod{ method = &serverconfigs.HTTPAuthBasicMethod{
Users: users, Users: users,
@@ -188,8 +199,9 @@ func (this *CreatePopupAction) RunPost(params struct {
Charset: params.BasicAuthCharset, Charset: params.BasicAuthCharset,
} }
case serverconfigs.HTTPAuthTypeSubRequest: case serverconfigs.HTTPAuthTypeSubRequest:
params.Must.Field("subRequestURL", params.SubRequestURL). params.Must.
Require("请输入子请求URL") Field("subRequestURL", params.SubRequestURL).
Require("请输入子请求 URL")
if params.SubRequestFollowRequest { if params.SubRequestFollowRequest {
params.SubRequestMethod = "" params.SubRequestMethod = ""
} }
@@ -198,7 +210,7 @@ func (this *CreatePopupAction) RunPost(params struct {
Method: params.SubRequestMethod, Method: params.SubRequestMethod,
} }
default: default:
this.Fail("不支持的鉴权类型'" + params.Type + "'") this.Fail("不支持的鉴权类型 '" + params.Type + "'")
} }
if method == nil { if method == nil {
@@ -214,7 +226,7 @@ func (this *CreatePopupAction) RunPost(params struct {
return return
} }
var paramsMap = maps.Map{} paramsMap := maps.Map{}
err = json.Unmarshal(methodJSON, &paramsMap) err = json.Unmarshal(methodJSON, &paramsMap)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
@@ -231,6 +243,7 @@ func (this *CreatePopupAction) RunPost(params struct {
return return
} }
defer this.CreateLogInfo(codes.HTTPAuthPolicy_LogCreateHTTPAuthPolicy, createResp.HttpAuthPolicyId) defer this.CreateLogInfo(codes.HTTPAuthPolicy_LogCreateHTTPAuthPolicy, createResp.HttpAuthPolicyId)
ref.AuthPolicyId = createResp.HttpAuthPolicyId ref.AuthPolicyId = createResp.HttpAuthPolicyId
ref.AuthPolicy = &serverconfigs.HTTPAuthPolicy{ ref.AuthPolicy = &serverconfigs.HTTPAuthPolicy{
Id: createResp.HttpAuthPolicyId, Id: createResp.HttpAuthPolicyId,

View File

@@ -27,7 +27,7 @@ func (this *UpdatePopupAction) Init() {
func (this *UpdatePopupAction) RunGet(params struct { func (this *UpdatePopupAction) RunGet(params struct {
PolicyId int64 PolicyId int64
}) { }) {
var authMethods = []*serverconfigs.HTTPAuthTypeDefinition{} authMethods := []*serverconfigs.HTTPAuthTypeDefinition{}
for _, method := range serverconfigs.FindAllHTTPAuthTypes(teaconst.Role) { for _, method := range serverconfigs.FindAllHTTPAuthTypes(teaconst.Role) {
if !method.IsPlus || (method.IsPlus && teaconst.IsPlus) { if !method.IsPlus || (method.IsPlus && teaconst.IsPlus) {
authMethods = append(authMethods, method) authMethods = append(authMethods, method)
@@ -47,7 +47,7 @@ func (this *UpdatePopupAction) RunGet(params struct {
return return
} }
var authParams = map[string]interface{}{} authParams := map[string]interface{}{}
if len(policy.ParamsJSON) > 0 { if len(policy.ParamsJSON) > 0 {
err = json.Unmarshal(policy.ParamsJSON, &authParams) err = json.Unmarshal(policy.ParamsJSON, &authParams)
if err != nil { if err != nil {
@@ -91,6 +91,10 @@ func (this *UpdatePopupAction) RunPost(params struct {
TypeDTimestampParamName string TypeDTimestampParamName string
TypeDLife int TypeDLife int
// TypeE
TypeESecret string
TypeELife int
// BasicAuth // BasicAuth
HttpAuthBasicAuthUsersJSON []byte HttpAuthBasicAuthUsersJSON []byte
BasicAuthRealm string BasicAuthRealm string
@@ -125,29 +129,25 @@ func (this *UpdatePopupAction) RunPost(params struct {
Field("name", params.Name). Field("name", params.Name).
Require("请输入名称") Require("请输入名称")
var ref = &serverconfigs.HTTPAuthPolicyRef{IsOn: true} ref := &serverconfigs.HTTPAuthPolicyRef{IsOn: true}
var method serverconfigs.HTTPAuthMethodInterface var method serverconfigs.HTTPAuthMethodInterface
// 扩展名 exts := utils.NewStringsStream(params.Exts).
var exts = utils.NewStringsStream(params.Exts).
Map(strings.TrimSpace, strings.ToLower). Map(strings.TrimSpace, strings.ToLower).
Filter(utils.FilterNotEmpty). Filter(utils.FilterNotEmpty).
Map(utils.MapAddPrefixFunc(".")). Map(utils.MapAddPrefixFunc(".")).
Unique(). Unique().
Result() Result()
// 域名 domains := []string{}
var domains = []string{}
if len(params.DomainsJSON) > 0 { if len(params.DomainsJSON) > 0 {
var rawDomains = []string{} rawDomains := []string{}
err := json.Unmarshal(params.DomainsJSON, &rawDomains) err := json.Unmarshal(params.DomainsJSON, &rawDomains)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
// TODO 如果用户填写了一个网址,应该分析域名并填入
domains = utils.NewStringsStream(rawDomains). domains = utils.NewStringsStream(rawDomains).
Map(strings.TrimSpace, strings.ToLower). Map(strings.TrimSpace, strings.ToLower).
Filter(utils.FilterNotEmpty). Filter(utils.FilterNotEmpty).
@@ -160,11 +160,11 @@ func (this *UpdatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeASecret", params.TypeASecret). Field("typeASecret", params.TypeASecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字"). Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字").
Field("typeASignParamName", params.TypeASignParamName). Field("typeASignParamName", params.TypeASignParamName).
Require("请输入签名参数"). Require("请输入签名参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线") Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线")
if params.TypeALife < 0 { if params.TypeALife < 0 {
params.TypeALife = 0 params.TypeALife = 0
@@ -179,8 +179,8 @@ func (this *UpdatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeBSecret", params.TypeBSecret). Field("typeBSecret", params.TypeBSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字") Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字")
method = &serverconfigs.HTTPAuthTypeBMethod{ method = &serverconfigs.HTTPAuthTypeBMethod{
Secret: params.TypeBSecret, Secret: params.TypeBSecret,
@@ -190,8 +190,8 @@ func (this *UpdatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeCSecret", params.TypeCSecret). Field("typeCSecret", params.TypeCSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字") Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字")
method = &serverconfigs.HTTPAuthTypeCMethod{ method = &serverconfigs.HTTPAuthTypeCMethod{
Secret: params.TypeCSecret, Secret: params.TypeCSecret,
@@ -201,14 +201,14 @@ func (this *UpdatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("typeDSecret", params.TypeDSecret). Field("typeDSecret", params.TypeDSecret).
Require("请输入鉴权密钥"). Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥不能超过40个字符"). MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字"). Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母数字").
Field("typeDSignParamName", params.TypeDSignParamName). Field("typeDSignParamName", params.TypeDSignParamName).
Require("请输入签名参数"). Require("请输入签名参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线"). Match(`^[a-zA-Z0-9_]{1,40}$`, "签名参数中只能包含字母、数字下划线").
Field("typeDTimestampParamName", params.TypeDTimestampParamName). Field("typeDTimestampParamName", params.TypeDTimestampParamName).
Require("请输入时间参数"). Require("请输入时间参数").
Match(`^[a-zA-Z0-9_]{1,40}$`, "时间参数中只能包含字母、数字下划线") Match(`^[a-zA-Z0-9_]{1,40}$`, "时间参数中只能包含字母、数字下划线")
method = &serverconfigs.HTTPAuthTypeDMethod{ method = &serverconfigs.HTTPAuthTypeDMethod{
Secret: params.TypeDSecret, Secret: params.TypeDSecret,
@@ -216,6 +216,17 @@ func (this *UpdatePopupAction) RunPost(params struct {
TimestampParamName: params.TypeDTimestampParamName, TimestampParamName: params.TypeDTimestampParamName,
Life: params.TypeDLife, Life: params.TypeDLife,
} }
case serverconfigs.HTTPAuthTypeTypeE:
params.Must.
Field("typeESecret", params.TypeESecret).
Require("请输入鉴权密钥").
MaxLength(40, "鉴权密钥长度不能超过40个字符").
Match(`^[a-zA-Z0-9]{1,40}$`, "鉴权密钥中只能包含字母和数字")
method = &serverconfigs.HTTPAuthTypeEMethod{
Secret: params.TypeESecret,
Life: params.TypeELife,
}
case serverconfigs.HTTPAuthTypeBasicAuth: case serverconfigs.HTTPAuthTypeBasicAuth:
users := []*serverconfigs.HTTPAuthBasicMethodUser{} users := []*serverconfigs.HTTPAuthBasicMethodUser{}
err := json.Unmarshal(params.HttpAuthBasicAuthUsersJSON, &users) err := json.Unmarshal(params.HttpAuthBasicAuthUsersJSON, &users)
@@ -224,7 +235,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
return return
} }
if len(users) == 0 { if len(users) == 0 {
this.Fail("请添加至少一个用户") this.Fail("请至少添加一个用户")
} }
method = &serverconfigs.HTTPAuthBasicMethod{ method = &serverconfigs.HTTPAuthBasicMethod{
Users: users, Users: users,
@@ -232,8 +243,9 @@ func (this *UpdatePopupAction) RunPost(params struct {
Charset: params.BasicAuthCharset, Charset: params.BasicAuthCharset,
} }
case serverconfigs.HTTPAuthTypeSubRequest: case serverconfigs.HTTPAuthTypeSubRequest:
params.Must.Field("subRequestURL", params.SubRequestURL). params.Must.
Require("请输入子请求URL") Field("subRequestURL", params.SubRequestURL).
Require("请输入子请求 URL")
if params.SubRequestFollowRequest { if params.SubRequestFollowRequest {
params.SubRequestMethod = "" params.SubRequestMethod = ""
} }
@@ -242,7 +254,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
Method: params.SubRequestMethod, Method: params.SubRequestMethod,
} }
default: default:
this.Fail("不支持的鉴权类型'" + policyType + "'") this.Fail("不支持的鉴权类型 '" + policyType + "'")
} }
if method == nil { if method == nil {
@@ -275,6 +287,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
ref.AuthPolicy = &serverconfigs.HTTPAuthPolicy{ ref.AuthPolicy = &serverconfigs.HTTPAuthPolicy{
Id: params.PolicyId, Id: params.PolicyId,
Name: params.Name, Name: params.Name,

View File

@@ -16,6 +16,8 @@ import (
"github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"net/http" "net/http"
"os"
"path/filepath"
) )
type ComponentsAction actions.Action type ComponentsAction actions.Action
@@ -41,12 +43,7 @@ func (this *ComponentsAction) RunGet(params struct{}) {
var buffer = bytes.NewBuffer([]byte{}) var buffer = bytes.NewBuffer([]byte{})
var webRoot string webRoot := findComponentsRoot()
if Tea.IsTesting() {
webRoot = Tea.Root + "/../web/public/js/components/"
} else {
webRoot = Tea.Root + "/web/public/js/components/"
}
f := files.NewFile(webRoot) f := files.NewFile(webRoot)
f.Range(func(file *files.File) { f.Range(func(file *files.File) {
@@ -173,3 +170,27 @@ func (this *ComponentsAction) RunGet(params struct{}) {
_, _ = this.Write(componentsData) _, _ = this.Write(componentsData)
} }
func findComponentsRoot() string {
candidates := []string{
filepath.Join(Tea.Root, "web", "public", "js", "components"),
filepath.Join(Tea.Root, "..", "web", "public", "js", "components"),
filepath.Join(Tea.Root, "..", "..", "web", "public", "js", "components"),
}
if cwd, err := os.Getwd(); err == nil && len(cwd) > 0 {
candidates = append(candidates,
filepath.Join(cwd, "web", "public", "js", "components"),
filepath.Join(cwd, "EdgeAdmin", "web", "public", "js", "components"),
)
}
for _, candidate := range candidates {
info, err := os.Stat(candidate)
if err == nil && info.IsDir() {
return candidate + string(filepath.Separator)
}
}
return filepath.Join(Tea.Root, "web", "public", "js", "components") + string(filepath.Separator)
}

View File

@@ -141,7 +141,6 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox"

View File

@@ -2272,6 +2272,57 @@ Vue.component("health-check-config-box", {
</div>` </div>`
}) })
Vue.component("httpdns-clusters-selector", {
props: ["vClusters", "vName"],
data: function () {
let inputClusters = this.vClusters
let clusters = []
if (inputClusters != null && inputClusters.length > 0) {
if (inputClusters[0].isChecked !== undefined) {
// 带 isChecked 标志的完整集群列表
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: c.isChecked}
})
} else {
// 仅包含已选集群,全部标记为选中
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: true}
})
}
}
// 无 prop 时从根实例读取所有集群(如创建应用页面)
if (clusters.length === 0) {
let rootClusters = this.$root.clusters
if (rootClusters != null && rootClusters.length > 0) {
clusters = rootClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: false}
})
}
}
return {
clusters: clusters,
fieldName: this.vName || "clusterIds"
}
},
methods: {
changeCluster: function (cluster) {
cluster.isChecked = !cluster.isChecked
}
},
template: `<div>
<div v-if="clusters.length > 0">
<checkbox v-for="cluster in clusters" :key="cluster.id" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" :name="fieldName">
{{cluster.name}}
</checkbox>
</div>
<span class="grey" v-else>暂无可用集群</span>
</div>`
})
/** /**
* 菜单项 * 菜单项
*/ */
@@ -11294,10 +11345,18 @@ Vue.component("http-auth-config-box", {
if (authConfig.policyRefs == null) { if (authConfig.policyRefs == null) {
authConfig.policyRefs = [] authConfig.policyRefs = []
} }
return { return {
authConfig: authConfig authConfig: authConfig
} }
}, },
watch: {
"authConfig.isOn": function () {
this.change()
},
"authConfig.isPrior": function () {
this.change()
}
},
methods: { methods: {
isOn: function () { isOn: function () {
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
@@ -11309,18 +11368,17 @@ Vue.component("http-auth-config-box", {
that.authConfig.policyRefs.push(resp.data.policyRef) that.authConfig.policyRefs.push(resp.data.policyRef)
that.change() that.change()
}, },
height: "28em" height: "32em"
}) })
}, },
update: function (index, policyId) { update: function (index, policyId) {
let that = this
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, { teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
callback: function (resp) { callback: function () {
teaweb.success("保存成功", function () { teaweb.success("保存成功", function () {
teaweb.reload() teaweb.reload()
}) })
}, },
height: "28em" height: "32em"
}) })
}, },
remove: function (index) { remove: function (index) {
@@ -11341,14 +11399,15 @@ Vue.component("http-auth-config-box", {
return "URL鉴权C" return "URL鉴权C"
case "typeD": case "typeD":
return "URL鉴权D" return "URL鉴权D"
case "typeE":
return "URL鉴权E"
} }
return "" return ""
}, },
change: function () { change: function () {
let that = this let that = this
setTimeout(function () { setTimeout(function () {
// 延时通知,是为了让表单有机会变更数据 that.$emit("change", that.authConfig)
that.$emit("change", this.authConfig)
}, 100) }, 100)
} }
}, },
@@ -11369,7 +11428,6 @@ Vue.component("http-auth-config-box", {
</tbody> </tbody>
</table> </table>
<div class="margin"></div> <div class="margin"></div>
<!-- 鉴权方式 -->
<div v-show="isOn()"> <div v-show="isOn()">
<h4>鉴权方式</h4> <h4>鉴权方式</h4>
<table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0"> <table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0">
@@ -11385,9 +11443,7 @@ Vue.component("http-auth-config-box", {
<tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId"> <tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId">
<tr> <tr>
<td>{{ref.authPolicy.name}}</td> <td>{{ref.authPolicy.name}}</td>
<td> <td>{{methodName(ref.authPolicy.type)}}</td>
{{methodName(ref.authPolicy.type)}}
</td>
<td> <td>
<span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span> <span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span>
<span v-if="ref.authPolicy.type == 'subRequest'"> <span v-if="ref.authPolicy.type == 'subRequest'">
@@ -11398,7 +11454,7 @@ Vue.component("http-auth-config-box", {
<span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeE'">路径模式/有效期{{ref.authPolicy.params.life}}秒</span>
<div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)"> <div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)">
<grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label> <grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label>
<grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label> <grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label>
@@ -11420,6 +11476,7 @@ Vue.component("http-auth-config-box", {
</div>` </div>`
}) })
Vue.component("http-cache-config-box", { Vue.component("http-cache-config-box", {
props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"],
data: function () { data: function () {

View File

@@ -2272,6 +2272,57 @@ Vue.component("health-check-config-box", {
</div>` </div>`
}) })
Vue.component("httpdns-clusters-selector", {
props: ["vClusters", "vName"],
data: function () {
let inputClusters = this.vClusters
let clusters = []
if (inputClusters != null && inputClusters.length > 0) {
if (inputClusters[0].isChecked !== undefined) {
// 带 isChecked 标志的完整集群列表
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: c.isChecked}
})
} else {
// 仅包含已选集群,全部标记为选中
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: true}
})
}
}
// 无 prop 时从根实例读取所有集群(如创建应用页面)
if (clusters.length === 0) {
let rootClusters = this.$root.clusters
if (rootClusters != null && rootClusters.length > 0) {
clusters = rootClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: false}
})
}
}
return {
clusters: clusters,
fieldName: this.vName || "clusterIds"
}
},
methods: {
changeCluster: function (cluster) {
cluster.isChecked = !cluster.isChecked
}
},
template: `<div>
<div v-if="clusters.length > 0">
<checkbox v-for="cluster in clusters" :key="cluster.id" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" :name="fieldName">
{{cluster.name}}
</checkbox>
</div>
<span class="grey" v-else>暂无可用集群</span>
</div>`
})
/** /**
* 菜单项 * 菜单项
*/ */
@@ -11294,10 +11345,18 @@ Vue.component("http-auth-config-box", {
if (authConfig.policyRefs == null) { if (authConfig.policyRefs == null) {
authConfig.policyRefs = [] authConfig.policyRefs = []
} }
return { return {
authConfig: authConfig authConfig: authConfig
} }
}, },
watch: {
"authConfig.isOn": function () {
this.change()
},
"authConfig.isPrior": function () {
this.change()
}
},
methods: { methods: {
isOn: function () { isOn: function () {
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
@@ -11309,18 +11368,17 @@ Vue.component("http-auth-config-box", {
that.authConfig.policyRefs.push(resp.data.policyRef) that.authConfig.policyRefs.push(resp.data.policyRef)
that.change() that.change()
}, },
height: "28em" height: "32em"
}) })
}, },
update: function (index, policyId) { update: function (index, policyId) {
let that = this
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, { teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
callback: function (resp) { callback: function () {
teaweb.success("保存成功", function () { teaweb.success("保存成功", function () {
teaweb.reload() teaweb.reload()
}) })
}, },
height: "28em" height: "32em"
}) })
}, },
remove: function (index) { remove: function (index) {
@@ -11341,14 +11399,15 @@ Vue.component("http-auth-config-box", {
return "URL鉴权C" return "URL鉴权C"
case "typeD": case "typeD":
return "URL鉴权D" return "URL鉴权D"
case "typeE":
return "URL鉴权E"
} }
return "" return ""
}, },
change: function () { change: function () {
let that = this let that = this
setTimeout(function () { setTimeout(function () {
// 延时通知,是为了让表单有机会变更数据 that.$emit("change", that.authConfig)
that.$emit("change", this.authConfig)
}, 100) }, 100)
} }
}, },
@@ -11369,7 +11428,6 @@ Vue.component("http-auth-config-box", {
</tbody> </tbody>
</table> </table>
<div class="margin"></div> <div class="margin"></div>
<!-- 鉴权方式 -->
<div v-show="isOn()"> <div v-show="isOn()">
<h4>鉴权方式</h4> <h4>鉴权方式</h4>
<table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0"> <table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0">
@@ -11385,9 +11443,7 @@ Vue.component("http-auth-config-box", {
<tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId"> <tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId">
<tr> <tr>
<td>{{ref.authPolicy.name}}</td> <td>{{ref.authPolicy.name}}</td>
<td> <td>{{methodName(ref.authPolicy.type)}}</td>
{{methodName(ref.authPolicy.type)}}
</td>
<td> <td>
<span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span> <span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span>
<span v-if="ref.authPolicy.type == 'subRequest'"> <span v-if="ref.authPolicy.type == 'subRequest'">
@@ -11398,7 +11454,7 @@ Vue.component("http-auth-config-box", {
<span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeE'">路径模式/有效期{{ref.authPolicy.params.life}}秒</span>
<div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)"> <div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)">
<grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label> <grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label>
<grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label> <grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label>
@@ -11420,6 +11476,7 @@ Vue.component("http-auth-config-box", {
</div>` </div>`
}) })
Vue.component("http-cache-config-box", { Vue.component("http-cache-config-box", {
props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"],
data: function () { data: function () {

View File

@@ -0,0 +1,49 @@
Vue.component("httpdns-clusters-selector", {
props: ["vClusters", "vName"],
data: function () {
let inputClusters = this.vClusters
let clusters = []
if (inputClusters != null && inputClusters.length > 0) {
if (inputClusters[0].isChecked !== undefined) {
// 带 isChecked 标志的完整集群列表
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: c.isChecked}
})
} else {
// 仅包含已选集群,全部标记为选中
clusters = inputClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: true}
})
}
}
// 无 prop 时从根实例读取所有集群(如创建应用页面)
if (clusters.length === 0) {
let rootClusters = this.$root.clusters
if (rootClusters != null && rootClusters.length > 0) {
clusters = rootClusters.map(function (c) {
return {id: c.id, name: c.name, isChecked: false}
})
}
}
return {
clusters: clusters,
fieldName: this.vName || "clusterIds"
}
},
methods: {
changeCluster: function (cluster) {
cluster.isChecked = !cluster.isChecked
}
},
template: `<div>
<div v-if="clusters.length > 0">
<checkbox v-for="cluster in clusters" :key="cluster.id" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" :name="fieldName">
{{cluster.name}}
</checkbox>
</div>
<span class="grey" v-else>暂无可用集群</span>
</div>`
})

View File

@@ -12,10 +12,18 @@ Vue.component("http-auth-config-box", {
if (authConfig.policyRefs == null) { if (authConfig.policyRefs == null) {
authConfig.policyRefs = [] authConfig.policyRefs = []
} }
return { return {
authConfig: authConfig authConfig: authConfig
} }
}, },
watch: {
"authConfig.isOn": function () {
this.change()
},
"authConfig.isPrior": function () {
this.change()
}
},
methods: { methods: {
isOn: function () { isOn: function () {
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
@@ -27,18 +35,17 @@ Vue.component("http-auth-config-box", {
that.authConfig.policyRefs.push(resp.data.policyRef) that.authConfig.policyRefs.push(resp.data.policyRef)
that.change() that.change()
}, },
height: "28em" height: "32em"
}) })
}, },
update: function (index, policyId) { update: function (index, policyId) {
let that = this
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, { teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
callback: function (resp) { callback: function () {
teaweb.success("保存成功", function () { teaweb.success("保存成功", function () {
teaweb.reload() teaweb.reload()
}) })
}, },
height: "28em" height: "32em"
}) })
}, },
remove: function (index) { remove: function (index) {
@@ -59,14 +66,15 @@ Vue.component("http-auth-config-box", {
return "URL鉴权C" return "URL鉴权C"
case "typeD": case "typeD":
return "URL鉴权D" return "URL鉴权D"
case "typeE":
return "URL鉴权E"
} }
return "" return ""
}, },
change: function () { change: function () {
let that = this let that = this
setTimeout(function () { setTimeout(function () {
// 延时通知,是为了让表单有机会变更数据 that.$emit("change", that.authConfig)
that.$emit("change", this.authConfig)
}, 100) }, 100)
} }
}, },
@@ -87,7 +95,6 @@ Vue.component("http-auth-config-box", {
</tbody> </tbody>
</table> </table>
<div class="margin"></div> <div class="margin"></div>
<!-- 鉴权方式 -->
<div v-show="isOn()"> <div v-show="isOn()">
<h4>鉴权方式</h4> <h4>鉴权方式</h4>
<table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0"> <table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0">
@@ -103,9 +110,7 @@ Vue.component("http-auth-config-box", {
<tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId"> <tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId">
<tr> <tr>
<td>{{ref.authPolicy.name}}</td> <td>{{ref.authPolicy.name}}</td>
<td> <td>{{methodName(ref.authPolicy.type)}}</td>
{{methodName(ref.authPolicy.type)}}
</td>
<td> <td>
<span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span> <span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span>
<span v-if="ref.authPolicy.type == 'subRequest'"> <span v-if="ref.authPolicy.type == 'subRequest'">
@@ -116,7 +121,7 @@ Vue.component("http-auth-config-box", {
<span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span> <span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span>
<span v-if="ref.authPolicy.type == 'typeE'">路径模式/有效期{{ref.authPolicy.params.life}}秒</span>
<div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)"> <div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)">
<grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label> <grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label>
<grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label> <grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label>

View File

@@ -37,7 +37,7 @@
</script> </script>
<script type="text/javascript" src="/js/config/brand.js"></script> <script type="text/javascript" src="/js/config/brand.js"></script>
<script type="text/javascript" src="/_/@default/@layout.js"></script> <script type="text/javascript" src="/_/@default/@layout.js"></script>
<script type="text/javascript" src="/js/components.js"></script> <script type="text/javascript" src="/js/components.js?v={$ .teaVersion}"></script>
<script type="text/javascript" src="/js/utils.min.js"></script> <script type="text/javascript" src="/js/utils.min.js"></script>
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script> <script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
<script type="text/javascript" src="/js/date.tea.js"></script> <script type="text/javascript" src="/js/date.tea.js"></script>

View File

@@ -1,32 +0,0 @@
{$layout}
{$template "../menu"}
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
<div class="margin"></div>
<warning-message>此功能为试验功能,目前只能做一些简单的网络数据包统计。</warning-message>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="clusterId" :value="clusterId"/>
<table class="ui table definition selectable">
<tr>
<td class="title">启用数据包统计</td>
<td>
<select class="ui dropdown auto-width" name="status" v-model="policy.status">
<option value="auto">自动</option>
<option value="on">启用</option>
<option value="off">停用</option>
</select>
<p class="comment" v-if="policy.status == 'auto'">自动根据服务器硬件配置决定是否启用,避免影响性能。</p>
<p class="comment" v-if="policy.status == 'on'">强制启用如果节点服务器配置较低可能会严重影响性能建议仅在8线程以上CPU节点服务器上选择强制启用。</p>
<p class="comment" v-if="policy.status == 'off'">完全关闭此功能。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</div>

View File

@@ -1,3 +0,0 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
})

View File

@@ -6,6 +6,7 @@
<span v-if="countTodayAttacks > 0" :class="{red: countTodayAttacks != countTodayAttacksRead}">({{countTodayAttacksFormat}})</span> <span v-if="countTodayAttacks > 0" :class="{red: countTodayAttacks != countTodayAttacksRead}">({{countTodayAttacksFormat}})</span>
</menu-item> </menu-item>
<menu-item href="/dashboard/boards/dns" code="dns">{{LANG('admin_dashboard@ui_dns')}}</menu-item> <menu-item href="/dashboard/boards/dns" code="dns">{{LANG('admin_dashboard@ui_dns')}}</menu-item>
<menu-item href="/dashboard/boards/httpdns" code="httpdns">HTTPDNS</menu-item>
<menu-item href="/dashboard/boards/user" code="user">{{LANG('admin_dashboard@ui_user')}}</menu-item> <menu-item href="/dashboard/boards/user" code="user">{{LANG('admin_dashboard@ui_user')}}</menu-item>
<menu-item href="/dashboard/boards/events" code="event">{{LANG('admin_dashboard@ui_events')}}<span :class="{red: countEvents > 0}">({{countEvents}})</span></menu-item> <menu-item href="/dashboard/boards/events" code="event">{{LANG('admin_dashboard@ui_events')}}<span :class="{red: countEvents > 0}">({{countEvents}})</span></menu-item>
</first-menu> </first-menu>

View File

@@ -0,0 +1,8 @@
.ui.message .icon {
position: absolute;
right: 1em;
top: 1.8em;
}
.chart-box {
height: 14em;
}

View File

@@ -0,0 +1,67 @@
{$layout}
{$template "menu"}
{$template "/echarts"}
<div style="margin-top: 0.8em" v-if="isLoading">
<div class="ui message loading">
<div class="ui active inline loader small"></div> &nbsp; 数据加载中...
</div>
</div>
<div v-show="!isLoading">
<columns-grid>
<div class="ui column">
<h4>应用<link-icon href="/httpdns/apps"></link-icon></h4>
<div class="value"><span>{{board.countApps}}</span></div>
</div>
<div class="ui column">
<h4>域名<link-icon href="/httpdns/apps"></link-icon></h4>
<div class="value"><span>{{board.countDomains}}</span></div>
</div>
<div class="ui column">
<h4>集群<link-icon href="/httpdns/clusters"></link-icon></h4>
<div class="value"><span>{{board.countClusters}}</span></div>
</div>
<div class="ui column">
<h4>节点<link-icon href="/httpdns/clusters"></link-icon></h4>
<div class="value"><span>{{board.countNodes}}</span>
<span v-if="board.countOfflineNodes > 0" style="font-size: 1em">
/ <a href="/httpdns/clusters"><span class="red" style="font-size: 1em">{{board.countOfflineNodes}}离线</span></a>
</span>
<span v-else style="font-size: 1em"></span>
</div>
</div>
</columns-grid>
<chart-columns-grid>
<div class="ui column">
<div class="ui menu text blue">
<a href="" class="item" :class="{active: trafficTab == 'hourly'}" @click.prevent="selectTrafficTab('hourly')">24小时请求趋势</a>
<a href="" class="item" :class="{active: trafficTab == 'daily'}" @click.prevent="selectTrafficTab('daily')">15天请求趋势</a>
</div>
<div class="ui divider"></div>
<div class="chart-box" id="hourly-traffic-chart" v-show="trafficTab == 'hourly'"></div>
<div class="chart-box" id="daily-traffic-chart" v-show="trafficTab == 'daily'"></div>
</div>
<div class="ui column">
<h4>应用请求排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-apps-chart"></div>
</div>
<div class="ui column">
<h4>域名请求排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-domains-chart"></div>
</div>
<div class="ui column">
<h4>节点访问排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-nodes-chart"></div>
</div>
</chart-columns-grid>
</div>

View File

@@ -0,0 +1,192 @@
Tea.context(function () {
this.isLoading = true
this.$delay(function () {
this.$post("$")
.success(function (resp) {
for (let k in resp.data) {
this[k] = resp.data[k]
}
this.$delay(function () {
this.reloadHourlyTrafficChart()
this.reloadTopAppsChart()
this.reloadTopDomainsChart()
this.reloadTopNodesChart()
})
this.isLoading = false
})
})
this.trafficTab = "hourly"
this.selectTrafficTab = function (tab) {
this.trafficTab = tab
if (tab == "hourly") {
this.$delay(function () {
this.reloadHourlyTrafficChart()
})
} else if (tab == "daily") {
this.$delay(function () {
this.reloadDailyTrafficChart()
})
}
}
this.reloadHourlyTrafficChart = function () {
let stats = this.hourlyStats
if (stats == null || stats.length == 0) {
return
}
this.reloadTrafficChart("hourly-traffic-chart", stats, function (args) {
return stats[args.dataIndex].day + " " + stats[args.dataIndex].hour + "时<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
})
}
this.reloadDailyTrafficChart = function () {
let stats = this.dailyStats
if (stats == null || stats.length == 0) {
return
}
this.reloadTrafficChart("daily-traffic-chart", stats, function (args) {
return stats[args.dataIndex].day + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
})
}
this.reloadTrafficChart = function (chartId, stats, tooltipFunc) {
let chartBox = document.getElementById(chartId)
if (chartBox == null) {
return
}
let axis = teaweb.countAxis(stats, function (v) {
return v.countRequests
})
let chart = teaweb.initChart(chartBox)
let option = {
xAxis: {
data: stats.map(function (v) {
if (v.hour != null) {
return v.hour
}
return v.day
})
},
yAxis: {
axisLabel: {
formatter: function (value) {
return value + axis.unit
}
}
},
tooltip: {
show: true,
trigger: "item",
formatter: tooltipFunc
},
grid: {
left: 50,
top: 10,
right: 20,
bottom: 20
},
series: [
{
name: "请求数",
type: "line",
data: stats.map(function (v) {
return v.countRequests / axis.divider
}),
itemStyle: {
color: teaweb.DefaultChartColor
},
areaStyle: {
color: teaweb.DefaultChartColor
},
smooth: true
}
],
animation: true
}
chart.setOption(option)
chart.resize()
}
this.reloadTopAppsChart = function () {
if (this.topAppStats == null || this.topAppStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topAppStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-apps-chart",
name: "应用",
values: this.topAppStats,
x: function (v) {
return v.appName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].appName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis
})
}
this.reloadTopDomainsChart = function () {
if (this.topDomainStats == null || this.topDomainStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topDomainStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-domains-chart",
name: "域名",
values: this.topDomainStats,
x: function (v) {
return v.domainName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].domainName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis
})
}
this.reloadTopNodesChart = function () {
if (this.topNodeStats == null || this.topNodeStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topNodeStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-nodes-chart",
name: "节点",
values: this.topNodeStats,
x: function (v) {
return v.nodeName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].nodeName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis,
click: function (args, stats) {
window.location = "/httpdns/clusters/cluster/node?nodeId=" + stats[args.dataIndex].nodeId + "&clusterId=" + stats[args.dataIndex].clusterId
}
})
}
})

View File

@@ -53,8 +53,9 @@
<!-- 升级提醒 --> <!-- 升级提醒 -->
<div class="ui icon message error" v-if="!isLoading && nodeUpgradeInfo.count > 0"> <div class="ui icon message error" v-if="!isLoading && nodeUpgradeInfo.count > 0">
<i class="icon warning circle"></i> <i class="icon warning circle"></i>
<a href="/clusters"> <a href="/clusters" v-if="autoUpgrade">升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a>
升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a><a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a> <a href="/settings/upgrade" v-else>升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,请到系统设置-升级设置页面自行升级...</a>
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div> </div>
<div class="ui icon message error" v-if="!isLoading && userNodeUpgradeInfo.count > 0 && teaIsPlus"> <div class="ui icon message error" v-if="!isLoading && userNodeUpgradeInfo.count > 0 && teaIsPlus">
<i class="icon warning circle"></i> <i class="icon warning circle"></i>
@@ -73,7 +74,15 @@
<div class="ui icon message error" v-if="!isLoading && nsNodeUpgradeInfo.count > 0 && teaIsPlus"> <div class="ui icon message error" v-if="!isLoading && nsNodeUpgradeInfo.count > 0 && teaIsPlus">
<i class="icon warning circle"></i> <i class="icon warning circle"></i>
<a href="/ns/clusters">升级提醒:有 {{nsNodeUpgradeInfo.count}} 个DNS节点需要升级到 v{{nsNodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a><a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a> <a href="/ns/clusters" v-if="autoUpgrade">升级提醒:有 {{nsNodeUpgradeInfo.count}} 个DNS节点需要升级到 v{{nsNodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a>
<a href="/settings/upgrade" v-else>升级提醒:有 {{nsNodeUpgradeInfo.count}} 个DNS节点需要升级到 v{{nsNodeUpgradeInfo.version}} 版本,请到系统设置-升级设置页面自行升级...</a>
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div>
<div class="ui icon message error" v-if="!isLoading && httpdnsNodeUpgradeInfo.count > 0 && teaIsPlus">
<i class="icon warning circle"></i>
<a href="/httpdns/clusters" v-if="autoUpgrade">升级提醒:有 {{httpdnsNodeUpgradeInfo.count}} 个HTTPDNS节点需要升级到 v{{httpdnsNodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a>
<a href="/settings/upgrade" v-else>升级提醒:有 {{httpdnsNodeUpgradeInfo.count}} 个HTTPDNS节点需要升级到 v{{httpdnsNodeUpgradeInfo.version}} 版本,请到系统设置-升级设置页面自行升级...</a>
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div> </div>
<div class="ui icon message error" v-if="!isLoading && reportNodeUpgradeInfo.count > 0 && teaIsPlus"> <div class="ui icon message error" v-if="!isLoading && reportNodeUpgradeInfo.count > 0 && teaIsPlus">
<i class="icon warning circle"></i> <i class="icon warning circle"></i>

View File

@@ -33,7 +33,9 @@
<!-- 边缘节点升级提醒 --> <!-- 边缘节点升级提醒 -->
<div class="ui icon message error" v-if="!isLoading && nodeUpgradeInfo.count > 0"> <div class="ui icon message error" v-if="!isLoading && nodeUpgradeInfo.count > 0">
<i class="icon warning circle"></i> <i class="icon warning circle"></i>
<a href="/clusters">升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a><a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a> <a href="/clusters" v-if="autoUpgrade">升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级,请耐心等待...</a>
<a href="/settings/upgrade" v-else>升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,请到系统设置-升级设置页面自行升级...</a>
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div> </div>
<!-- API节点升级提醒 --> <!-- API节点升级提醒 -->

View File

@@ -1,6 +1,9 @@
Tea.context(function () { Tea.context(function () {
this.isOn = true this.isOn = true
this.syncDefaultClusterEnabled = function () {
}
this.success = function (resp) { this.success = function (resp) {
let clusterId = 0 let clusterId = 0
if (resp != null && resp.data != null && typeof resp.data.clusterId != "undefined") { if (resp != null && resp.data != null && typeof resp.data.clusterId != "undefined") {

View File

@@ -0,0 +1,8 @@
.ui.message .icon {
position: absolute;
right: 1em;
top: 1.8em;
}
.chart-box {
height: 14em;
}

View File

@@ -0,0 +1,62 @@
{$layout}
{$template "/echarts"}
<div style="margin-top: 0.8em" v-if="isLoading">
<div class="ui message loading">
<div class="ui active inline loader small"></div> &nbsp; 数据加载中...
</div>
</div>
<div v-show="!isLoading">
<div class="ui four columns grid counter-chart">
<div class="ui column">
<h4>应用<link-icon href="/httpdns/apps"></link-icon></h4>
<div class="value"><span>{{board.countApps}}</span></div>
</div>
<div class="ui column">
<h4>域名<link-icon href="/httpdns/apps"></link-icon></h4>
<div class="value"><span>{{board.countDomains}}</span></div>
</div>
<div class="ui column">
<h4>集群<link-icon href="/httpdns/clusters"></link-icon></h4>
<div class="value"><span>{{board.countClusters}}</span></div>
</div>
<div class="ui column with-border">
<h4>节点<link-icon href="/httpdns/clusters"></link-icon></h4>
<div class="value"><span>{{board.countNodes}}</span>
<span v-if="board.countOfflineNodes > 0" class="red" style="font-size: 1em">{{board.countOfflineNodes}}离线</span>
<span v-else style="font-size: 1em"></span>
</div>
</div>
</div>
<div class="ui four columns grid chart-grid" style="margin-top: 1em">
<div class="ui column">
<div class="ui menu text blue small">
<a href="" class="item" :class="{active: trafficTab == 'hourly'}" @click.prevent="selectTrafficTab('hourly')">24小时请求趋势</a>
<a href="" class="item" :class="{active: trafficTab == 'daily'}" @click.prevent="selectTrafficTab('daily')">15天请求趋势</a>
</div>
<div class="ui divider"></div>
<div class="chart-box" id="hourly-traffic-chart" v-show="trafficTab == 'hourly'"></div>
<div class="chart-box" id="daily-traffic-chart" v-show="trafficTab == 'daily'"></div>
</div>
<div class="ui column">
<h4>应用请求排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-apps-chart"></div>
</div>
<div class="ui column">
<h4>域名请求排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-domains-chart"></div>
</div>
<div class="ui column">
<h4>节点访问排行 <span>24小时</span></h4>
<div class="ui divider"></div>
<div class="chart-box" id="top-nodes-chart"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,191 @@
Tea.context(function () {
this.isLoading = true
this.$delay(function () {
this.$post("$")
.success(function (resp) {
for (let k in resp.data) {
this[k] = resp.data[k]
}
this.$delay(function () {
this.reloadHourlyTrafficChart()
this.reloadTopAppsChart()
this.reloadTopDomainsChart()
this.reloadTopNodesChart()
})
this.isLoading = false
})
})
this.trafficTab = "hourly"
this.selectTrafficTab = function (tab) {
this.trafficTab = tab
if (tab == "hourly") {
this.$delay(function () {
this.reloadHourlyTrafficChart()
})
} else if (tab == "daily") {
this.$delay(function () {
this.reloadDailyTrafficChart()
})
}
}
this.reloadHourlyTrafficChart = function () {
let stats = this.hourlyStats
if (stats == null || stats.length == 0) {
return
}
this.reloadTrafficChart("hourly-traffic-chart", stats, function (args) {
return stats[args.dataIndex].day + " " + stats[args.dataIndex].hour + "时<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
})
}
this.reloadDailyTrafficChart = function () {
let stats = this.dailyStats
if (stats == null || stats.length == 0) {
return
}
this.reloadTrafficChart("daily-traffic-chart", stats, function (args) {
return stats[args.dataIndex].day + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
})
}
this.reloadTrafficChart = function (chartId, stats, tooltipFunc) {
let chartBox = document.getElementById(chartId)
if (chartBox == null) {
return
}
let axis = teaweb.countAxis(stats, function (v) {
return v.countRequests
})
let chart = teaweb.initChart(chartBox)
let option = {
xAxis: {
data: stats.map(function (v) {
if (v.hour != null) {
return v.hour
}
return v.day
})
},
yAxis: {
axisLabel: {
formatter: function (value) {
return value + axis.unit
}
}
},
tooltip: {
show: true,
trigger: "item",
formatter: tooltipFunc
},
grid: {
left: 50,
top: 10,
right: 20,
bottom: 20
},
series: [
{
name: "请求数",
type: "line",
data: stats.map(function (v) {
return v.countRequests / axis.divider
}),
itemStyle: {
color: teaweb.DefaultChartColor
},
areaStyle: {
color: teaweb.DefaultChartColor
},
smooth: true
}
],
animation: true
}
chart.setOption(option)
chart.resize()
}
this.reloadTopAppsChart = function () {
if (this.topAppStats == null || this.topAppStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topAppStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-apps-chart",
name: "应用",
values: this.topAppStats,
x: function (v) {
return v.appName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].appName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis
})
}
this.reloadTopDomainsChart = function () {
if (this.topDomainStats == null || this.topDomainStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topDomainStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-domains-chart",
name: "域名",
values: this.topDomainStats,
x: function (v) {
return v.domainName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].domainName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis
})
}
this.reloadTopNodesChart = function () {
if (this.topNodeStats == null || this.topNodeStats.length == 0) {
return
}
let axis = teaweb.countAxis(this.topNodeStats, function (v) {
return v.countRequests
})
teaweb.renderBarChart({
id: "top-nodes-chart",
name: "节点",
values: this.topNodeStats,
x: function (v) {
return v.nodeName
},
tooltip: function (args, stats) {
return stats[args.dataIndex].nodeName + "<br/>请求数:" + teaweb.formatNumber(stats[args.dataIndex].countRequests)
},
value: function (v) {
return v.countRequests / axis.divider
},
axis: axis,
click: function (args, stats) {
window.location = "/httpdns/clusters/cluster/node?nodeId=" + stats[args.dataIndex].nodeId + "&clusterId=" + stats[args.dataIndex].clusterId
}
})
}
})

View File

@@ -124,6 +124,27 @@
</tr> </tr>
</tbody> </tbody>
<!-- TypeE -->
<tbody v-show="type == 'typeE'">
<tr>
<td>鉴权密钥 *</td>
<td>
<input type="text" maxlength="40" name="typeESecret" v-model="typeESecret" autocomplete="off"/>
<p class="comment">只能包含字母、数字长度不超过40。<a href="" @click.prevent="generateTypeESecret()">[随机生成]</a></p>
</td>
</tr>
<tr>
<td>有效时间</td>
<td>
<div class="ui input right labeled">
<input type="text" maxlength="8" name="typeELife" value="30" style="width: 7em"/>
<span class="ui label"></span>
</div>
<p class="comment">URL 格式:<code-label>/hash/timestamp/path</code-label>;签名算法:<code-label>md5(secret + uri + timestamp)</code-label>timestamp 为十六进制 Unix 秒数。</p>
</td>
</tr>
</tbody>
<!-- BasicAuth --> <!-- BasicAuth -->
<tbody v-show="type == 'basicAuth'"> <tbody v-show="type == 'basicAuth'">
<tr> <tr>
@@ -138,7 +159,7 @@
</td> </td>
</tr> </tr>
<tr v-show="moreBasicAuthOptionsVisible"> <tr v-show="moreBasicAuthOptionsVisible">
<td>认证领域名<em>Realm</em></td> <td>认证领域名 <em>(Realm)</em></td>
<td> <td>
<input type="text" name="basicAuthRealm" value="" maxlength="100"/> <input type="text" name="basicAuthRealm" value="" maxlength="100"/>
</td> </td>
@@ -147,7 +168,7 @@
<td>字符集</td> <td>字符集</td>
<td> <td>
<input type="text" name="basicAuthCharset" style="width: 6em" maxlength="50"/> <input type="text" name="basicAuthCharset" style="width: 6em" maxlength="50"/>
<p class="comment">类似于<code-label>UTF-8</code-label></p> <p class="comment">类似于 <code-label>UTF-8</code-label></p>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -186,7 +207,7 @@
<td>限定文件扩展名</td> <td>限定文件扩展名</td>
<td> <td>
<values-box name="exts"></values-box> <values-box name="exts"></values-box>
<p class="comment">如果不为空,则表示只有这些扩展名的文件才需要鉴权;扩展名需要包含点符号.,比如<code-label>.png</code-label></p> <p class="comment">如果不为空,则表示只有这些扩展名的文件才需要鉴权;扩展名需要包含点符号,比如 <code-label>.png</code-label></p>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -84,6 +84,18 @@ Tea.context(function () {
this.authDescription = this.authDescription.replace("t=", this.typeDTimestampParamName + "=") this.authDescription = this.authDescription.replace("t=", this.typeDTimestampParamName + "=")
} }
/**
* TypeE
*/
this.typeESecret = ""
this.generateTypeESecret = function () {
this.$post(".random")
.success(function (resp) {
this.typeESecret = resp.data.random
})
}
/** /**
* 基本认证 * 基本认证
*/ */

View File

@@ -122,6 +122,27 @@
</tr> </tr>
</tbody> </tbody>
<!-- TypeE -->
<tbody v-show="type == 'typeE'">
<tr>
<td>鉴权密钥 *</td>
<td>
<input type="text" maxlength="40" name="typeESecret" v-model="typeESecret" autocomplete="off"/>
<p class="comment">只能包含字母、数字长度不超过40。<a href="" @click.prevent="generateTypeESecret()">[随机生成]</a></p>
</td>
</tr>
<tr>
<td>有效时间</td>
<td>
<div class="ui input right labeled">
<input type="text" maxlength="8" name="typeELife" value="30" style="width: 7em" v-model="policy.params.life"/>
<span class="ui label"></span>
</div>
<p class="comment">URL 格式:<code-label>/hash/timestamp/path</code-label>;签名算法:<code-label>md5(secret + uri + timestamp)</code-label>timestamp 为十六进制 Unix 秒数。</p>
</td>
</tr>
</tbody>
<!-- BasicAuth --> <!-- BasicAuth -->
<tbody v-show="type == 'basicAuth'"> <tbody v-show="type == 'basicAuth'">
<tr> <tr>
@@ -136,7 +157,7 @@
</td> </td>
</tr> </tr>
<tr v-show="moreBasicAuthOptionsVisible"> <tr v-show="moreBasicAuthOptionsVisible">
<td>认证领域名<em>Realm</em></td> <td>认证领域名 <em>(Realm)</em></td>
<td> <td>
<input type="text" name="basicAuthRealm" value="" maxlength="100" v-model="policy.params.realm"/> <input type="text" name="basicAuthRealm" value="" maxlength="100" v-model="policy.params.realm"/>
</td> </td>
@@ -145,7 +166,7 @@
<td>字符集</td> <td>字符集</td>
<td> <td>
<input type="text" name="basicAuthCharset" style="width: 6em" v-model="policy.params.charset" maxlength="50"/> <input type="text" name="basicAuthCharset" style="width: 6em" v-model="policy.params.charset" maxlength="50"/>
<p class="comment">类似于<code-label>utf-8</code-label></p> <p class="comment">类似于 <code-label>utf-8</code-label></p>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -185,7 +206,7 @@
<td>限定文件扩展名</td> <td>限定文件扩展名</td>
<td> <td>
<values-box name="exts" :v-values="policy.params.exts"></values-box> <values-box name="exts" :v-values="policy.params.exts"></values-box>
<p class="comment">如果不为空,则表示只有这些扩展名的文件才需要鉴权;扩展名需要包含点符号.,比如<code-label>.png</code-label></p> <p class="comment">如果不为空,则表示只有这些扩展名的文件才需要鉴权;扩展名需要包含点符号,比如 <code-label>.png</code-label></p>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -115,6 +115,22 @@ Tea.context(function () {
this.authDescription = this.authDescription.replace("t=", this.typeDTimestampParamName + "=") this.authDescription = this.authDescription.replace("t=", this.typeDTimestampParamName + "=")
} }
/**
* TypeE
*/
this.typeESecret = ""
if (this.policy.type == "typeE") {
this.typeESecret = this.policy.params.secret
}
this.generateTypeESecret = function () {
this.$post(".random")
.success(function (resp) {
this.typeESecret = resp.data.random
})
}
/** /**
* 基本鉴权 * 基本鉴权
*/ */

View File

@@ -0,0 +1 @@
build/temp_build.sh

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,650 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v3.12.4
// source: service_httpdns_board.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// 组合看板数据请求
type ComposeHTTPDNSBoardRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardRequest) Reset() {
*x = ComposeHTTPDNSBoardRequest{}
mi := &file_service_httpdns_board_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardRequest) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardRequest) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardRequest.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardRequest) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{0}
}
// 组合看板数据响应
type ComposeHTTPDNSBoardResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
CountApps int64 `protobuf:"varint,1,opt,name=countApps,proto3" json:"countApps,omitempty"`
CountDomains int64 `protobuf:"varint,2,opt,name=countDomains,proto3" json:"countDomains,omitempty"`
CountClusters int64 `protobuf:"varint,3,opt,name=countClusters,proto3" json:"countClusters,omitempty"`
CountNodes int64 `protobuf:"varint,4,opt,name=countNodes,proto3" json:"countNodes,omitempty"`
CountOfflineNodes int64 `protobuf:"varint,5,opt,name=countOfflineNodes,proto3" json:"countOfflineNodes,omitempty"`
DailyTrafficStats []*ComposeHTTPDNSBoardResponse_DailyTrafficStat `protobuf:"bytes,30,rep,name=dailyTrafficStats,proto3" json:"dailyTrafficStats,omitempty"`
HourlyTrafficStats []*ComposeHTTPDNSBoardResponse_HourlyTrafficStat `protobuf:"bytes,31,rep,name=hourlyTrafficStats,proto3" json:"hourlyTrafficStats,omitempty"`
TopAppStats []*ComposeHTTPDNSBoardResponse_TopAppStat `protobuf:"bytes,32,rep,name=topAppStats,proto3" json:"topAppStats,omitempty"`
TopDomainStats []*ComposeHTTPDNSBoardResponse_TopDomainStat `protobuf:"bytes,33,rep,name=topDomainStats,proto3" json:"topDomainStats,omitempty"`
TopNodeStats []*ComposeHTTPDNSBoardResponse_TopNodeStat `protobuf:"bytes,34,rep,name=topNodeStats,proto3" json:"topNodeStats,omitempty"`
CpuNodeValues []*NodeValue `protobuf:"bytes,35,rep,name=cpuNodeValues,proto3" json:"cpuNodeValues,omitempty"`
MemoryNodeValues []*NodeValue `protobuf:"bytes,36,rep,name=memoryNodeValues,proto3" json:"memoryNodeValues,omitempty"`
LoadNodeValues []*NodeValue `protobuf:"bytes,37,rep,name=loadNodeValues,proto3" json:"loadNodeValues,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse) Reset() {
*x = ComposeHTTPDNSBoardResponse{}
mi := &file_service_httpdns_board_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1}
}
func (x *ComposeHTTPDNSBoardResponse) GetCountApps() int64 {
if x != nil {
return x.CountApps
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse) GetCountDomains() int64 {
if x != nil {
return x.CountDomains
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse) GetCountClusters() int64 {
if x != nil {
return x.CountClusters
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse) GetCountNodes() int64 {
if x != nil {
return x.CountNodes
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse) GetCountOfflineNodes() int64 {
if x != nil {
return x.CountOfflineNodes
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse) GetDailyTrafficStats() []*ComposeHTTPDNSBoardResponse_DailyTrafficStat {
if x != nil {
return x.DailyTrafficStats
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetHourlyTrafficStats() []*ComposeHTTPDNSBoardResponse_HourlyTrafficStat {
if x != nil {
return x.HourlyTrafficStats
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetTopAppStats() []*ComposeHTTPDNSBoardResponse_TopAppStat {
if x != nil {
return x.TopAppStats
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetTopDomainStats() []*ComposeHTTPDNSBoardResponse_TopDomainStat {
if x != nil {
return x.TopDomainStats
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetTopNodeStats() []*ComposeHTTPDNSBoardResponse_TopNodeStat {
if x != nil {
return x.TopNodeStats
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetCpuNodeValues() []*NodeValue {
if x != nil {
return x.CpuNodeValues
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetMemoryNodeValues() []*NodeValue {
if x != nil {
return x.MemoryNodeValues
}
return nil
}
func (x *ComposeHTTPDNSBoardResponse) GetLoadNodeValues() []*NodeValue {
if x != nil {
return x.LoadNodeValues
}
return nil
}
type ComposeHTTPDNSBoardResponse_DailyTrafficStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Day string `protobuf:"bytes,1,opt,name=day,proto3" json:"day,omitempty"`
Bytes int64 `protobuf:"varint,2,opt,name=bytes,proto3" json:"bytes,omitempty"`
CountRequests int64 `protobuf:"varint,3,opt,name=countRequests,proto3" json:"countRequests,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) Reset() {
*x = ComposeHTTPDNSBoardResponse_DailyTrafficStat{}
mi := &file_service_httpdns_board_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse_DailyTrafficStat) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse_DailyTrafficStat.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse_DailyTrafficStat) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1, 0}
}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) GetDay() string {
if x != nil {
return x.Day
}
return ""
}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) GetBytes() int64 {
if x != nil {
return x.Bytes
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_DailyTrafficStat) GetCountRequests() int64 {
if x != nil {
return x.CountRequests
}
return 0
}
type ComposeHTTPDNSBoardResponse_HourlyTrafficStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hour string `protobuf:"bytes,1,opt,name=hour,proto3" json:"hour,omitempty"`
Bytes int64 `protobuf:"varint,2,opt,name=bytes,proto3" json:"bytes,omitempty"`
CountRequests int64 `protobuf:"varint,3,opt,name=countRequests,proto3" json:"countRequests,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) Reset() {
*x = ComposeHTTPDNSBoardResponse_HourlyTrafficStat{}
mi := &file_service_httpdns_board_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse_HourlyTrafficStat) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse_HourlyTrafficStat.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse_HourlyTrafficStat) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1, 1}
}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) GetHour() string {
if x != nil {
return x.Hour
}
return ""
}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) GetBytes() int64 {
if x != nil {
return x.Bytes
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_HourlyTrafficStat) GetCountRequests() int64 {
if x != nil {
return x.CountRequests
}
return 0
}
type ComposeHTTPDNSBoardResponse_TopAppStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
AppId int64 `protobuf:"varint,1,opt,name=appId,proto3" json:"appId,omitempty"`
AppName string `protobuf:"bytes,2,opt,name=appName,proto3" json:"appName,omitempty"`
CountRequests int64 `protobuf:"varint,3,opt,name=countRequests,proto3" json:"countRequests,omitempty"`
Bytes int64 `protobuf:"varint,4,opt,name=bytes,proto3" json:"bytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) Reset() {
*x = ComposeHTTPDNSBoardResponse_TopAppStat{}
mi := &file_service_httpdns_board_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse_TopAppStat) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse_TopAppStat.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse_TopAppStat) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1, 2}
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) GetAppId() int64 {
if x != nil {
return x.AppId
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) GetAppName() string {
if x != nil {
return x.AppName
}
return ""
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) GetCountRequests() int64 {
if x != nil {
return x.CountRequests
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopAppStat) GetBytes() int64 {
if x != nil {
return x.Bytes
}
return 0
}
type ComposeHTTPDNSBoardResponse_TopDomainStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
DomainId int64 `protobuf:"varint,1,opt,name=domainId,proto3" json:"domainId,omitempty"`
DomainName string `protobuf:"bytes,2,opt,name=domainName,proto3" json:"domainName,omitempty"`
CountRequests int64 `protobuf:"varint,3,opt,name=countRequests,proto3" json:"countRequests,omitempty"`
Bytes int64 `protobuf:"varint,4,opt,name=bytes,proto3" json:"bytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) Reset() {
*x = ComposeHTTPDNSBoardResponse_TopDomainStat{}
mi := &file_service_httpdns_board_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse_TopDomainStat) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse_TopDomainStat.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse_TopDomainStat) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1, 3}
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) GetDomainId() int64 {
if x != nil {
return x.DomainId
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) GetDomainName() string {
if x != nil {
return x.DomainName
}
return ""
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) GetCountRequests() int64 {
if x != nil {
return x.CountRequests
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopDomainStat) GetBytes() int64 {
if x != nil {
return x.Bytes
}
return 0
}
type ComposeHTTPDNSBoardResponse_TopNodeStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClusterId int64 `protobuf:"varint,1,opt,name=clusterId,proto3" json:"clusterId,omitempty"`
NodeId int64 `protobuf:"varint,2,opt,name=nodeId,proto3" json:"nodeId,omitempty"`
NodeName string `protobuf:"bytes,3,opt,name=nodeName,proto3" json:"nodeName,omitempty"`
CountRequests int64 `protobuf:"varint,4,opt,name=countRequests,proto3" json:"countRequests,omitempty"`
Bytes int64 `protobuf:"varint,5,opt,name=bytes,proto3" json:"bytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) Reset() {
*x = ComposeHTTPDNSBoardResponse_TopNodeStat{}
mi := &file_service_httpdns_board_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ComposeHTTPDNSBoardResponse_TopNodeStat) ProtoMessage() {}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) ProtoReflect() protoreflect.Message {
mi := &file_service_httpdns_board_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ComposeHTTPDNSBoardResponse_TopNodeStat.ProtoReflect.Descriptor instead.
func (*ComposeHTTPDNSBoardResponse_TopNodeStat) Descriptor() ([]byte, []int) {
return file_service_httpdns_board_proto_rawDescGZIP(), []int{1, 4}
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) GetClusterId() int64 {
if x != nil {
return x.ClusterId
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) GetNodeId() int64 {
if x != nil {
return x.NodeId
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) GetNodeName() string {
if x != nil {
return x.NodeName
}
return ""
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) GetCountRequests() int64 {
if x != nil {
return x.CountRequests
}
return 0
}
func (x *ComposeHTTPDNSBoardResponse_TopNodeStat) GetBytes() int64 {
if x != nil {
return x.Bytes
}
return 0
}
var File_service_httpdns_board_proto protoreflect.FileDescriptor
const file_service_httpdns_board_proto_rawDesc = "" +
"\n" +
"\x1bservice_httpdns_board.proto\x12\x02pb\x1a\x1dmodels/model_node_value.proto\"\x1c\n" +
"\x1aComposeHTTPDNSBoardRequest\"\x9c\v\n" +
"\x1bComposeHTTPDNSBoardResponse\x12\x1c\n" +
"\tcountApps\x18\x01 \x01(\x03R\tcountApps\x12\"\n" +
"\fcountDomains\x18\x02 \x01(\x03R\fcountDomains\x12$\n" +
"\rcountClusters\x18\x03 \x01(\x03R\rcountClusters\x12\x1e\n" +
"\n" +
"countNodes\x18\x04 \x01(\x03R\n" +
"countNodes\x12,\n" +
"\x11countOfflineNodes\x18\x05 \x01(\x03R\x11countOfflineNodes\x12^\n" +
"\x11dailyTrafficStats\x18\x1e \x03(\v20.pb.ComposeHTTPDNSBoardResponse.DailyTrafficStatR\x11dailyTrafficStats\x12a\n" +
"\x12hourlyTrafficStats\x18\x1f \x03(\v21.pb.ComposeHTTPDNSBoardResponse.HourlyTrafficStatR\x12hourlyTrafficStats\x12L\n" +
"\vtopAppStats\x18 \x03(\v2*.pb.ComposeHTTPDNSBoardResponse.TopAppStatR\vtopAppStats\x12U\n" +
"\x0etopDomainStats\x18! \x03(\v2-.pb.ComposeHTTPDNSBoardResponse.TopDomainStatR\x0etopDomainStats\x12O\n" +
"\ftopNodeStats\x18\" \x03(\v2+.pb.ComposeHTTPDNSBoardResponse.TopNodeStatR\ftopNodeStats\x123\n" +
"\rcpuNodeValues\x18# \x03(\v2\r.pb.NodeValueR\rcpuNodeValues\x129\n" +
"\x10memoryNodeValues\x18$ \x03(\v2\r.pb.NodeValueR\x10memoryNodeValues\x125\n" +
"\x0eloadNodeValues\x18% \x03(\v2\r.pb.NodeValueR\x0eloadNodeValues\x1a`\n" +
"\x10DailyTrafficStat\x12\x10\n" +
"\x03day\x18\x01 \x01(\tR\x03day\x12\x14\n" +
"\x05bytes\x18\x02 \x01(\x03R\x05bytes\x12$\n" +
"\rcountRequests\x18\x03 \x01(\x03R\rcountRequests\x1ac\n" +
"\x11HourlyTrafficStat\x12\x12\n" +
"\x04hour\x18\x01 \x01(\tR\x04hour\x12\x14\n" +
"\x05bytes\x18\x02 \x01(\x03R\x05bytes\x12$\n" +
"\rcountRequests\x18\x03 \x01(\x03R\rcountRequests\x1ax\n" +
"\n" +
"TopAppStat\x12\x14\n" +
"\x05appId\x18\x01 \x01(\x03R\x05appId\x12\x18\n" +
"\aappName\x18\x02 \x01(\tR\aappName\x12$\n" +
"\rcountRequests\x18\x03 \x01(\x03R\rcountRequests\x12\x14\n" +
"\x05bytes\x18\x04 \x01(\x03R\x05bytes\x1a\x87\x01\n" +
"\rTopDomainStat\x12\x1a\n" +
"\bdomainId\x18\x01 \x01(\x03R\bdomainId\x12\x1e\n" +
"\n" +
"domainName\x18\x02 \x01(\tR\n" +
"domainName\x12$\n" +
"\rcountRequests\x18\x03 \x01(\x03R\rcountRequests\x12\x14\n" +
"\x05bytes\x18\x04 \x01(\x03R\x05bytes\x1a\x9b\x01\n" +
"\vTopNodeStat\x12\x1c\n" +
"\tclusterId\x18\x01 \x01(\x03R\tclusterId\x12\x16\n" +
"\x06nodeId\x18\x02 \x01(\x03R\x06nodeId\x12\x1a\n" +
"\bnodeName\x18\x03 \x01(\tR\bnodeName\x12$\n" +
"\rcountRequests\x18\x04 \x01(\x03R\rcountRequests\x12\x14\n" +
"\x05bytes\x18\x05 \x01(\x03R\x05bytes2m\n" +
"\x13HTTPDNSBoardService\x12V\n" +
"\x13composeHTTPDNSBoard\x12\x1e.pb.ComposeHTTPDNSBoardRequest\x1a\x1f.pb.ComposeHTTPDNSBoardResponseB\x06Z\x04./pbb\x06proto3"
var (
file_service_httpdns_board_proto_rawDescOnce sync.Once
file_service_httpdns_board_proto_rawDescData []byte
)
func file_service_httpdns_board_proto_rawDescGZIP() []byte {
file_service_httpdns_board_proto_rawDescOnce.Do(func() {
file_service_httpdns_board_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_service_httpdns_board_proto_rawDesc), len(file_service_httpdns_board_proto_rawDesc)))
})
return file_service_httpdns_board_proto_rawDescData
}
var file_service_httpdns_board_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_service_httpdns_board_proto_goTypes = []any{
(*ComposeHTTPDNSBoardRequest)(nil), // 0: pb.ComposeHTTPDNSBoardRequest
(*ComposeHTTPDNSBoardResponse)(nil), // 1: pb.ComposeHTTPDNSBoardResponse
(*ComposeHTTPDNSBoardResponse_DailyTrafficStat)(nil), // 2: pb.ComposeHTTPDNSBoardResponse.DailyTrafficStat
(*ComposeHTTPDNSBoardResponse_HourlyTrafficStat)(nil), // 3: pb.ComposeHTTPDNSBoardResponse.HourlyTrafficStat
(*ComposeHTTPDNSBoardResponse_TopAppStat)(nil), // 4: pb.ComposeHTTPDNSBoardResponse.TopAppStat
(*ComposeHTTPDNSBoardResponse_TopDomainStat)(nil), // 5: pb.ComposeHTTPDNSBoardResponse.TopDomainStat
(*ComposeHTTPDNSBoardResponse_TopNodeStat)(nil), // 6: pb.ComposeHTTPDNSBoardResponse.TopNodeStat
(*NodeValue)(nil), // 7: pb.NodeValue
}
var file_service_httpdns_board_proto_depIdxs = []int32{
2, // 0: pb.ComposeHTTPDNSBoardResponse.dailyTrafficStats:type_name -> pb.ComposeHTTPDNSBoardResponse.DailyTrafficStat
3, // 1: pb.ComposeHTTPDNSBoardResponse.hourlyTrafficStats:type_name -> pb.ComposeHTTPDNSBoardResponse.HourlyTrafficStat
4, // 2: pb.ComposeHTTPDNSBoardResponse.topAppStats:type_name -> pb.ComposeHTTPDNSBoardResponse.TopAppStat
5, // 3: pb.ComposeHTTPDNSBoardResponse.topDomainStats:type_name -> pb.ComposeHTTPDNSBoardResponse.TopDomainStat
6, // 4: pb.ComposeHTTPDNSBoardResponse.topNodeStats:type_name -> pb.ComposeHTTPDNSBoardResponse.TopNodeStat
7, // 5: pb.ComposeHTTPDNSBoardResponse.cpuNodeValues:type_name -> pb.NodeValue
7, // 6: pb.ComposeHTTPDNSBoardResponse.memoryNodeValues:type_name -> pb.NodeValue
7, // 7: pb.ComposeHTTPDNSBoardResponse.loadNodeValues:type_name -> pb.NodeValue
0, // 8: pb.HTTPDNSBoardService.composeHTTPDNSBoard:input_type -> pb.ComposeHTTPDNSBoardRequest
1, // 9: pb.HTTPDNSBoardService.composeHTTPDNSBoard:output_type -> pb.ComposeHTTPDNSBoardResponse
9, // [9:10] is the sub-list for method output_type
8, // [8:9] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_service_httpdns_board_proto_init() }
func file_service_httpdns_board_proto_init() {
if File_service_httpdns_board_proto != nil {
return
}
file_models_model_node_value_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_service_httpdns_board_proto_rawDesc), len(file_service_httpdns_board_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_service_httpdns_board_proto_goTypes,
DependencyIndexes: file_service_httpdns_board_proto_depIdxs,
MessageInfos: file_service_httpdns_board_proto_msgTypes,
}.Build()
File_service_httpdns_board_proto = out.File
file_service_httpdns_board_proto_goTypes = nil
file_service_httpdns_board_proto_depIdxs = nil
}

View File

@@ -0,0 +1,125 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v3.12.4
// source: service_httpdns_board.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
HTTPDNSBoardService_ComposeHTTPDNSBoard_FullMethodName = "/pb.HTTPDNSBoardService/composeHTTPDNSBoard"
)
// HTTPDNSBoardServiceClient is the client API for HTTPDNSBoardService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// HTTPDNS 看板服务
type HTTPDNSBoardServiceClient interface {
// 组合看板数据
ComposeHTTPDNSBoard(ctx context.Context, in *ComposeHTTPDNSBoardRequest, opts ...grpc.CallOption) (*ComposeHTTPDNSBoardResponse, error)
}
type hTTPDNSBoardServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHTTPDNSBoardServiceClient(cc grpc.ClientConnInterface) HTTPDNSBoardServiceClient {
return &hTTPDNSBoardServiceClient{cc}
}
func (c *hTTPDNSBoardServiceClient) ComposeHTTPDNSBoard(ctx context.Context, in *ComposeHTTPDNSBoardRequest, opts ...grpc.CallOption) (*ComposeHTTPDNSBoardResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ComposeHTTPDNSBoardResponse)
err := c.cc.Invoke(ctx, HTTPDNSBoardService_ComposeHTTPDNSBoard_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// HTTPDNSBoardServiceServer is the server API for HTTPDNSBoardService service.
// All implementations should embed UnimplementedHTTPDNSBoardServiceServer
// for forward compatibility.
//
// HTTPDNS 看板服务
type HTTPDNSBoardServiceServer interface {
// 组合看板数据
ComposeHTTPDNSBoard(context.Context, *ComposeHTTPDNSBoardRequest) (*ComposeHTTPDNSBoardResponse, error)
}
// UnimplementedHTTPDNSBoardServiceServer should be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedHTTPDNSBoardServiceServer struct{}
func (UnimplementedHTTPDNSBoardServiceServer) ComposeHTTPDNSBoard(context.Context, *ComposeHTTPDNSBoardRequest) (*ComposeHTTPDNSBoardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ComposeHTTPDNSBoard not implemented")
}
func (UnimplementedHTTPDNSBoardServiceServer) testEmbeddedByValue() {}
// UnsafeHTTPDNSBoardServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HTTPDNSBoardServiceServer will
// result in compilation errors.
type UnsafeHTTPDNSBoardServiceServer interface {
mustEmbedUnimplementedHTTPDNSBoardServiceServer()
}
func RegisterHTTPDNSBoardServiceServer(s grpc.ServiceRegistrar, srv HTTPDNSBoardServiceServer) {
// If the following call panics, it indicates UnimplementedHTTPDNSBoardServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&HTTPDNSBoardService_ServiceDesc, srv)
}
func _HTTPDNSBoardService_ComposeHTTPDNSBoard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ComposeHTTPDNSBoardRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HTTPDNSBoardServiceServer).ComposeHTTPDNSBoard(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HTTPDNSBoardService_ComposeHTTPDNSBoard_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HTTPDNSBoardServiceServer).ComposeHTTPDNSBoard(ctx, req.(*ComposeHTTPDNSBoardRequest))
}
return interceptor(ctx, in, info, handler)
}
// HTTPDNSBoardService_ServiceDesc is the grpc.ServiceDesc for HTTPDNSBoardService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var HTTPDNSBoardService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "pb.HTTPDNSBoardService",
HandlerType: (*HTTPDNSBoardServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "composeHTTPDNSBoard",
Handler: _HTTPDNSBoardService_ComposeHTTPDNSBoard_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "service_httpdns_board.proto",
}

View File

@@ -12,6 +12,7 @@ import (
) )
var httpAuthTimestampRegexp = regexp.MustCompile(`^\d{10}$`) var httpAuthTimestampRegexp = regexp.MustCompile(`^\d{10}$`)
var httpAuthHexTimestampRegexp = regexp.MustCompile(`^[0-9a-fA-F]{1,16}$`)
type HTTPAuthBaseMethod struct { type HTTPAuthBaseMethod struct {
Exts []string `json:"exts"` Exts []string `json:"exts"`

View File

@@ -7,12 +7,12 @@ type HTTPAuthType = string
const ( const (
HTTPAuthTypeBasicAuth HTTPAuthType = "basicAuth" // BasicAuth HTTPAuthTypeBasicAuth HTTPAuthType = "basicAuth" // BasicAuth
HTTPAuthTypeSubRequest HTTPAuthType = "subRequest" // 子请求 HTTPAuthTypeSubRequest HTTPAuthType = "subRequest" // SubRequest
HTTPAuthTypeTypeA HTTPAuthType = "typeA"
HTTPAuthTypeTypeA HTTPAuthType = "typeA" HTTPAuthTypeTypeB HTTPAuthType = "typeB"
HTTPAuthTypeTypeB HTTPAuthType = "typeB" HTTPAuthTypeTypeC HTTPAuthType = "typeC"
HTTPAuthTypeTypeC HTTPAuthType = "typeC" HTTPAuthTypeTypeD HTTPAuthType = "typeD"
HTTPAuthTypeTypeD HTTPAuthType = "typeD" HTTPAuthTypeTypeE HTTPAuthType = "typeE"
) )
type HTTPAuthTypeDefinition struct { type HTTPAuthTypeDefinition struct {
@@ -23,10 +23,11 @@ type HTTPAuthTypeDefinition struct {
} }
func FindAllHTTPAuthTypes(role string) []*HTTPAuthTypeDefinition { func FindAllHTTPAuthTypes(role string) []*HTTPAuthTypeDefinition {
var urlDocA = "" urlDocA := ""
var urlDocB = "" urlDocB := ""
var urlDocC = "" urlDocC := ""
var urlDocD = "" urlDocD := ""
urlDocE := "https://help.aliyun.com/zh/cdn/user-guide/type-c-signing"
switch role { switch role {
case "admin": case "admin":
@@ -66,15 +67,21 @@ func FindAllHTTPAuthTypes(role string) []*HTTPAuthTypeDefinition {
Description: "示例URLhttps://example.com/images/test.jpg?sign=f66af42f87cf63a64f4b86ec11c7797a&t=1661753717<br/><a href=\"" + urlDocD + "\" target=\"_blank\">[算法详解]</a>", Description: "示例URLhttps://example.com/images/test.jpg?sign=f66af42f87cf63a64f4b86ec11c7797a&t=1661753717<br/><a href=\"" + urlDocD + "\" target=\"_blank\">[算法详解]</a>",
IsPlus: true, IsPlus: true,
}, },
{
Name: "URL鉴权E",
Code: HTTPAuthTypeTypeE,
Description: "严格兼容阿里云 Type-C 路径鉴权示例URLhttps://example.com/3a2c79f2d2f0df2f8f9e05ec9f482e5d/67cfdb9e/images/test.jpg<br/><a href=\"" + urlDocE + "\" target=\"_blank\">[阿里云文档]</a>",
IsPlus: true,
},
{ {
Name: "基本认证", Name: "基本认证",
Code: HTTPAuthTypeBasicAuth, Code: HTTPAuthTypeBasicAuth,
Description: "BasicAuth最简单的HTTP请求认证方式通过传递<span class=\"ui label tiny basic text\">Authorization: Basic xxx</span> Header认证。", Description: "BasicAuth最简单的 HTTP 请求认证方式,通过传递 <span class=\"ui label tiny basic text\">Authorization: Basic xxx</span> Header 认证。",
}, },
{ {
Name: "子请求", Name: "子请求",
Code: HTTPAuthTypeSubRequest, Code: HTTPAuthTypeSubRequest,
Description: "通过自定义的URL子请求来认证请求。", Description: "通过自定义的 URL 子请求来认证请求。",
}, },
} }
} }

View File

@@ -22,6 +22,8 @@ func (this *HTTPAuthPolicy) Init() error {
this.method = NewHTTPAuthTypeCMethod() this.method = NewHTTPAuthTypeCMethod()
case HTTPAuthTypeTypeD: case HTTPAuthTypeTypeD:
this.method = NewHTTPAuthTypeDMethod() this.method = NewHTTPAuthTypeDMethod()
case HTTPAuthTypeTypeE:
this.method = NewHTTPAuthTypeEMethod()
} }
if this.method == nil { if this.method == nil {

View File

@@ -1,5 +1,138 @@
#!/usr/bin/env bash #!/usr/bin/env bash
function find-binary() {
local candidate
for candidate in "$@"; do
if [ -z "$candidate" ]; then
continue
fi
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
return 1
}
function host-goarch() {
case "$(uname -m)" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
i386|i486|i586|i686)
echo "386"
;;
mips64)
echo "mips64"
;;
mips64el)
echo "mips64le"
;;
*)
echo ""
;;
esac
}
function find-linux-static-toolchain() {
local arch=$1
local cc_bin=""
local cxx_bin=""
local host_arch
host_arch=$(host-goarch)
case "$arch" in
amd64)
cc_bin=$(find-binary \
"/usr/local/gcc/x86_64-unknown-linux-gnu/bin/x86_64-unknown-linux-gnu-gcc" \
"/usr/local/opt/musl-cross/bin/x86_64-linux-musl-gcc" \
"x86_64-unknown-linux-gnu-gcc" \
"x86_64-linux-musl-gcc" \
"musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/gcc/x86_64-unknown-linux-gnu/bin/x86_64-unknown-linux-gnu-g++" \
"/usr/local/opt/musl-cross/bin/x86_64-linux-musl-g++" \
"x86_64-unknown-linux-gnu-g++" \
"x86_64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "amd64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "amd64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
386)
cc_bin=$(find-binary "/usr/local/opt/musl-cross/bin/i486-linux-musl-gcc" "i486-linux-musl-gcc")
cxx_bin=$(find-binary "/usr/local/opt/musl-cross/bin/i486-linux-musl-g++" "i486-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "386" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "386" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
arm64)
cc_bin=$(find-binary \
"/usr/local/gcc/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-gcc" \
"/usr/local/opt/musl-cross/bin/aarch64-linux-musl-gcc" \
"aarch64-unknown-linux-gnu-gcc" \
"aarch64-linux-musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/gcc/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-g++" \
"/usr/local/opt/musl-cross/bin/aarch64-linux-musl-g++" \
"aarch64-unknown-linux-gnu-g++" \
"aarch64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "arm64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "arm64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
mips64)
cc_bin=$(find-binary "/usr/local/opt/musl-cross/bin/mips64-linux-musl-gcc" "mips64-linux-musl-gcc")
cxx_bin=$(find-binary "/usr/local/opt/musl-cross/bin/mips64-linux-musl-g++" "mips64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "mips64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "mips64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
mips64le)
cc_bin=$(find-binary "/usr/local/opt/musl-cross/bin/mips64el-linux-musl-gcc" "mips64el-linux-musl-gcc")
cxx_bin=$(find-binary "/usr/local/opt/musl-cross/bin/mips64el-linux-musl-g++" "mips64el-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "mips64le" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "mips64le" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
*)
return 1
;;
esac
if [ -z "$cc_bin" ]; then
return 1
fi
if [ -z "$cxx_bin" ]; then
cxx_bin="$cc_bin"
fi
echo "$cc_bin|$cxx_bin"
return 0
}
function build() { function build() {
ROOT=$(dirname "$0") ROOT=$(dirname "$0")
NAME="edge-dns" NAME="edge-dns"
@@ -18,10 +151,9 @@ function build() {
fi fi
echo "checking ..." echo "checking ..."
ZIP_PATH=$(which zip) if ! command -v zip >/dev/null 2>&1; then
if [ -z "$ZIP_PATH" ]; then
echo "we need 'zip' command to compress files" echo "we need 'zip' command to compress files"
exit exit 1
fi fi
echo "building v${VERSION}/${OS}/${ARCH} ..." echo "building v${VERSION}/${OS}/${ARCH} ..."
@@ -37,38 +169,20 @@ function build() {
fi fi
cp "$ROOT"/configs/api_dns.template.yaml "$DIST"/configs cp "$ROOT"/configs/api_dns.template.yaml "$DIST"/configs
copy_fluent_bit_assets "$ROOT" "$DIST" "$OS" "$ARCH" || exit 1
echo "building ..." echo "building ..."
MUSL_DIR="/usr/local/opt/musl-cross/bin" if [ "$OS" == "linux" ]; then
CC_PATH="" TOOLCHAIN=$(find-linux-static-toolchain "$ARCH")
CXX_PATH="" if [ -z "$TOOLCHAIN" ]; then
if [[ $(uname -a) == *"Darwin"* && "${OS}" == "linux" ]]; then echo "could not find a static Linux toolchain for ${ARCH}"
# /usr/local/opt/musl-cross/bin/ echo "install a musl toolchain before building edge-dns"
if [ "${ARCH}" == "amd64" ]; then exit 1
CC_PATH="x86_64-linux-musl-gcc"
CXX_PATH="x86_64-linux-musl-g++"
fi fi
if [ "${ARCH}" == "386" ]; then
CC_PATH="i486-linux-musl-gcc" CC_BIN=${TOOLCHAIN%|*}
CXX_PATH="i486-linux-musl-g++" CXX_BIN=${TOOLCHAIN#*|}
fi env CC="$CC_BIN" CXX="$CXX_BIN" GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 go build -trimpath -tags="plus" -o "$DIST"/bin/${NAME} -ldflags "-linkmode external -extldflags -static -s -w" "$ROOT"/../cmd/edge-dns/main.go
if [ "${ARCH}" == "arm64" ]; then
CC_PATH="aarch64-linux-musl-gcc"
CXX_PATH="aarch64-linux-musl-g++"
fi
if [ "${ARCH}" == "mips64" ]; then
CC_PATH="mips64-linux-musl-gcc"
CXX_PATH="mips64-linux-musl-g++"
fi
if [ "${ARCH}" == "mips64le" ]; then
CC_PATH="mips64el-linux-musl-gcc"
CXX_PATH="mips64el-linux-musl-g++"
fi
fi
if [ ! -z $CC_PATH ]; then
env CC=$MUSL_DIR/$CC_PATH CXX=$MUSL_DIR/$CXX_PATH GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 go build -trimpath -tags="plus" -o "$DIST"/bin/${NAME} -ldflags "-linkmode external -extldflags -static -s -w" "$ROOT"/../cmd/edge-dns/main.go
else else
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 go build -trimpath -tags="plus" -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-dns/main.go env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 go build -trimpath -tags="plus" -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-dns/main.go
fi fi
@@ -76,7 +190,7 @@ function build() {
# check build result # check build result
RESULT=$? RESULT=$?
if [ "${RESULT}" != "0" ]; then if [ "${RESULT}" != "0" ]; then
exit exit 1
fi fi
# delete hidden files # delete hidden files
@@ -95,81 +209,6 @@ function build() {
echo "OK" echo "OK"
} }
function copy_fluent_bit_assets() {
ROOT=$1
DIST=$2
OS=$3
ARCH=$4
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/deploy/fluent-bit"
if [ ! -d "$FLUENT_ROOT" ]; then
echo "[error] fluent-bit source directory not found: $FLUENT_ROOT"
return 1
fi
verify_fluent_bit_package_matrix "$FLUENT_ROOT" "$ARCH" || return 1
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
for file in fluent-bit.conf fluent-bit-dns.conf fluent-bit-https.conf fluent-bit-dns-https.conf fluent-bit-windows.conf fluent-bit-windows-https.conf parsers.conf clickhouse-upstream.conf clickhouse-upstream-windows.conf README.md; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ "$OS" = "linux" ]; then
PACKAGE_SRC="$FLUENT_ROOT/packages/linux-$ARCH"
PACKAGE_DST="$FLUENT_DIST/packages/linux-$ARCH"
if [ -d "$PACKAGE_SRC" ]; then
mkdir -p "$PACKAGE_DST"
cp -R "$PACKAGE_SRC/." "$PACKAGE_DST/"
else
echo "[error] fluent-bit package directory not found: $PACKAGE_SRC"
return 1
fi
fi
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
return 0
}
function verify_fluent_bit_package_matrix() {
FLUENT_ROOT=$1
ARCH=$2
REQUIRED_FILES=()
if [ "$ARCH" = "amd64" ]; then
REQUIRED_FILES=(
"packages/linux-amd64/fluent-bit_4.2.2_amd64.deb"
"packages/linux-amd64/fluent-bit-4.2.2-1.x86_64.rpm"
)
elif [ "$ARCH" = "arm64" ]; then
REQUIRED_FILES=(
"packages/linux-arm64/fluent-bit_4.2.2_arm64.deb"
"packages/linux-arm64/fluent-bit-4.2.2-1.aarch64.rpm"
)
else
echo "[error] unsupported arch for fluent-bit package validation: $ARCH"
return 1
fi
MISSING=0
for FILE in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$FLUENT_ROOT/$FILE" ]; then
echo "[error] fluent-bit matrix package missing: $FLUENT_ROOT/$FILE"
MISSING=1
fi
done
if [ "$MISSING" -ne 0 ]; then
return 1
fi
return 0
}
function lookup-version() { function lookup-version() {
FILE=$1 FILE=$1
VERSION_DATA=$(cat "$FILE") VERSION_DATA=$(cat "$FILE")
@@ -179,7 +218,7 @@ function lookup-version() {
echo "$VERSION" echo "$VERSION"
else else
echo "could not match version" echo "could not match version"
exit exit 1
fi fi
} }

View File

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

View File

@@ -34,10 +34,14 @@ function build() {
mkdir -p "$DIST"/data mkdir -p "$DIST"/data
cp "$ROOT"/configs/api_httpdns.template.yaml "$DIST"/configs cp "$ROOT"/configs/api_httpdns.template.yaml "$DIST"/configs
copy_fluent_bit_assets "$ROOT" "$DIST" "$OS" "$ARCH" || exit 1
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 \ if [ "${OS}" = "linux" ]; then
go build -trimpath -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-httpdns/main.go env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=0 \
go build -trimpath -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-httpdns/main.go
else
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 \
go build -trimpath -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-httpdns/main.go
fi
if [ ! -f "$DIST"/bin/${NAME} ]; then if [ ! -f "$DIST"/bin/${NAME} ]; then
echo "build failed!" echo "build failed!"
@@ -60,81 +64,6 @@ function build() {
echo "OK" echo "OK"
} }
function copy_fluent_bit_assets() {
ROOT=$1
DIST=$2
OS=$3
ARCH=$4
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/deploy/fluent-bit"
if [ ! -d "$FLUENT_ROOT" ]; then
echo "[error] fluent-bit source directory not found: $FLUENT_ROOT"
return 1
fi
verify_fluent_bit_package_matrix "$FLUENT_ROOT" "$ARCH" || return 1
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
for file in fluent-bit.conf fluent-bit-dns.conf fluent-bit-https.conf fluent-bit-dns-https.conf fluent-bit-windows.conf fluent-bit-windows-https.conf parsers.conf clickhouse-upstream.conf clickhouse-upstream-windows.conf README.md; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ "$OS" = "linux" ]; then
PACKAGE_SRC="$FLUENT_ROOT/packages/linux-$ARCH"
PACKAGE_DST="$FLUENT_DIST/packages/linux-$ARCH"
if [ -d "$PACKAGE_SRC" ]; then
mkdir -p "$PACKAGE_DST"
cp -R "$PACKAGE_SRC/." "$PACKAGE_DST/"
else
echo "[error] fluent-bit package directory not found: $PACKAGE_SRC"
return 1
fi
fi
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
return 0
}
function verify_fluent_bit_package_matrix() {
FLUENT_ROOT=$1
ARCH=$2
REQUIRED_FILES=()
if [ "$ARCH" = "amd64" ]; then
REQUIRED_FILES=(
"packages/linux-amd64/fluent-bit_4.2.2_amd64.deb"
"packages/linux-amd64/fluent-bit-4.2.2-1.x86_64.rpm"
)
elif [ "$ARCH" = "arm64" ]; then
REQUIRED_FILES=(
"packages/linux-arm64/fluent-bit_4.2.2_arm64.deb"
"packages/linux-arm64/fluent-bit-4.2.2-1.aarch64.rpm"
)
else
echo "[error] unsupported arch for fluent-bit package validation: $ARCH"
return 1
fi
MISSING=0
for FILE in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$FLUENT_ROOT/$FILE" ]; then
echo "[error] fluent-bit matrix package missing: $FLUENT_ROOT/$FILE"
MISSING=1
fi
done
if [ "$MISSING" -ne 0 ]; then
return 1
fi
return 0
}
function lookup-version() { function lookup-version() {
FILE=$1 FILE=$1
VERSION_DATA=$(cat "$FILE") VERSION_DATA=$(cat "$FILE")

View File

@@ -8,21 +8,29 @@ require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000 github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6 github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
github.com/miekg/dns v1.1.72
github.com/shirou/gopsutil/v3 v3.24.5
golang.org/x/sys v0.39.0
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/miekg/dns v1.1.72 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/oschwald/geoip2-golang v1.13.0 // indirect github.com/oschwald/geoip2-golang v1.13.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/mod v0.31.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
) )

View File

@@ -1,9 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -16,12 +21,32 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
@@ -36,22 +61,19 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=

View File

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

View File

@@ -15,18 +15,28 @@ import (
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const" teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc" "github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
"github.com/TeaOSLab/EdgeHttpDNS/internal/utils" "github.com/TeaOSLab/EdgeHttpDNS/internal/utils"
"github.com/iwind/TeaGo/maps"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
) )
type StatusManager struct { type StatusManager struct {
quitCh <-chan struct{} quitCh <-chan struct{}
ticker *time.Ticker ticker *time.Ticker
isFirstTime bool
cpuUpdatedTime time.Time
cpuLogicalCount int
cpuPhysicalCount int
} }
func NewStatusManager(quitCh <-chan struct{}) *StatusManager { func NewStatusManager(quitCh <-chan struct{}) *StatusManager {
return &StatusManager{ return &StatusManager{
quitCh: quitCh, quitCh: quitCh,
ticker: time.NewTicker(30 * time.Second), ticker: time.NewTicker(30 * time.Second),
isFirstTime: true,
cpuUpdatedTime: time.Now(),
cpuLogicalCount: runtime.NumCPU(),
} }
} }
@@ -72,10 +82,12 @@ func (m *StatusManager) update() {
IsActive: true, IsActive: true,
StatusJSON: statusJSON, StatusJSON: statusJSON,
}) })
if err != nil { if err != nil {
log.Println("[HTTPDNS_NODE][status]update status failed:", err.Error()) log.Println("[HTTPDNS_NODE][status]update status failed:", err.Error())
} }
m.reportNodeValues(rpcClient, status)
m.isFirstTime = false
} }
func (m *StatusManager) collectStatus() *nodeconfigs.NodeStatus { func (m *StatusManager) collectStatus() *nodeconfigs.NodeStatus {
@@ -87,8 +99,8 @@ func (m *StatusManager) collectStatus() *nodeconfigs.NodeStatus {
ConfigVersion: 0, ConfigVersion: 0,
OS: runtime.GOOS, OS: runtime.GOOS,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
CPULogicalCount: runtime.NumCPU(), CPULogicalCount: m.cpuLogicalCount,
CPUPhysicalCount: runtime.NumCPU(), CPUPhysicalCount: m.cpuPhysicalCount,
IsActive: true, IsActive: true,
ConnectionCount: 0, ConnectionCount: 0,
UpdatedAt: now, UpdatedAt: now,
@@ -104,26 +116,104 @@ func (m *StatusManager) collectStatus() *nodeconfigs.NodeStatus {
} }
} }
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
status.Hostname = hostname status.Hostname = hostname
exePath, _ := os.Executable() exePath, _ := os.Executable()
status.ExePath = exePath status.ExePath = exePath
var memStats runtime.MemStats m.updateCPU(status)
runtime.ReadMemStats(&memStats) m.updateMemory(status)
status.MemoryTotal = memStats.Sys m.updateLoad(status)
if status.MemoryTotal > 0 {
status.MemoryUsage = float64(memStats.Alloc) / float64(status.MemoryTotal) return status
}
func (m *StatusManager) updateCPU(status *nodeconfigs.NodeStatus) {
duration := time.Duration(0)
if m.isFirstTime {
duration = 100 * time.Millisecond
} }
percents, err := cpu.Percent(duration, false)
if err == nil && len(percents) > 0 {
status.CPUUsage = percents[0] / 100
}
if time.Since(m.cpuUpdatedTime) >= 5*time.Minute || m.cpuLogicalCount <= 0 {
m.cpuUpdatedTime = time.Now()
if logicalCount, countErr := cpu.Counts(true); countErr == nil && logicalCount > 0 {
m.cpuLogicalCount = logicalCount
}
if physicalCount, countErr := cpu.Counts(false); countErr == nil && physicalCount > 0 {
m.cpuPhysicalCount = physicalCount
}
}
status.CPULogicalCount = m.cpuLogicalCount
if m.cpuPhysicalCount > 0 {
status.CPUPhysicalCount = m.cpuPhysicalCount
}
}
func (m *StatusManager) updateMemory(status *nodeconfigs.NodeStatus) {
stat, err := mem.VirtualMemory()
if err != nil || stat == nil || stat.Total == 0 {
return
}
usedBytes := stat.Used
if stat.Total > stat.Free+stat.Buffers+stat.Cached {
usedBytes = stat.Total - stat.Free - stat.Buffers - stat.Cached
}
status.MemoryTotal = stat.Total
status.MemoryUsage = float64(usedBytes) / float64(stat.Total)
}
func (m *StatusManager) updateLoad(status *nodeconfigs.NodeStatus) {
load1m, load5m, load15m := readLoadAvg() load1m, load5m, load15m := readLoadAvg()
status.Load1m = load1m status.Load1m = load1m
status.Load5m = load5m status.Load5m = load5m
status.Load15m = load15m status.Load15m = load15m
}
return status func (m *StatusManager) reportNodeValues(rpcClient *rpc.RPCClient, status *nodeconfigs.NodeStatus) {
if rpcClient == nil || status == nil {
return
}
m.createNodeValue(rpcClient, nodeconfigs.NodeValueItemCPU, maps.Map{
"usage": status.CPUUsage,
"cores": status.CPULogicalCount,
})
m.createNodeValue(rpcClient, nodeconfigs.NodeValueItemMemory, maps.Map{
"usage": status.MemoryUsage,
"total": status.MemoryTotal,
})
m.createNodeValue(rpcClient, nodeconfigs.NodeValueItemLoad, maps.Map{
"load1m": status.Load1m,
"load5m": status.Load5m,
"load15m": status.Load15m,
})
}
func (m *StatusManager) createNodeValue(rpcClient *rpc.RPCClient, item string, value maps.Map) {
valueJSON, err := json.Marshal(value)
if err != nil {
log.Println("[HTTPDNS_NODE][status]marshal node value failed:", err.Error())
return
}
_, err = rpcClient.NodeValueRPC.CreateNodeValue(rpcClient.Context(), &pb.CreateNodeValueRequest{
Item: item,
ValueJSON: valueJSON,
CreatedAt: time.Now().Unix(),
})
if err != nil {
log.Println("[HTTPDNS_NODE][status]create node value failed:", item, err.Error())
}
} }
func readLoadAvg() (float64, float64, float64) { func readLoadAvg() (float64, float64, float64) {

View File

@@ -26,13 +26,13 @@ import (
"sync/atomic" "sync/atomic"
) )
type RPCClient struct { type RPCClient struct {
apiConfig *configs.APIConfig apiConfig *configs.APIConfig
conns []*grpc.ClientConn conns []*grpc.ClientConn
locker sync.RWMutex locker sync.RWMutex
NodeTaskRPC pb.NodeTaskServiceClient NodeTaskRPC pb.NodeTaskServiceClient
NodeValueRPC pb.NodeValueServiceClient
HTTPDNSNodeRPC pb.HTTPDNSNodeServiceClient HTTPDNSNodeRPC pb.HTTPDNSNodeServiceClient
HTTPDNSClusterRPC pb.HTTPDNSClusterServiceClient HTTPDNSClusterRPC pb.HTTPDNSClusterServiceClient
HTTPDNSAppRPC pb.HTTPDNSAppServiceClient HTTPDNSAppRPC pb.HTTPDNSAppServiceClient
@@ -47,7 +47,6 @@ type RPCClient struct {
totalCostMs int64 totalCostMs int64
} }
func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) { func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
if apiConfig == nil { if apiConfig == nil {
return nil, errors.New("api config should not be nil") return nil, errors.New("api config should not be nil")
@@ -55,6 +54,7 @@ func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
client := &RPCClient{apiConfig: apiConfig} client := &RPCClient{apiConfig: apiConfig}
client.NodeTaskRPC = pb.NewNodeTaskServiceClient(client) client.NodeTaskRPC = pb.NewNodeTaskServiceClient(client)
client.NodeValueRPC = pb.NewNodeValueServiceClient(client)
client.HTTPDNSNodeRPC = pb.NewHTTPDNSNodeServiceClient(client) client.HTTPDNSNodeRPC = pb.NewHTTPDNSNodeServiceClient(client)
client.HTTPDNSClusterRPC = pb.NewHTTPDNSClusterServiceClient(client) client.HTTPDNSClusterRPC = pb.NewHTTPDNSClusterServiceClient(client)
client.HTTPDNSAppRPC = pb.NewHTTPDNSAppServiceClient(client) client.HTTPDNSAppRPC = pb.NewHTTPDNSAppServiceClient(client)
@@ -212,7 +212,6 @@ func (c *RPCClient) GetAndResetMetrics() (total int64, failed int64, avgCostSeco
return return
} }
func (c *RPCClient) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { func (c *RPCClient) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
conn := c.pickConn() conn := c.pickConn()
if conn == nil { if conn == nil {

2
EdgeNode/.gitignore vendored
View File

@@ -8,6 +8,8 @@ configs/api_node.yaml
# Build binaries # Build binaries
bin/ bin/
libs/
.third_party_build/
# Runtime Data # Runtime Data
data/ data/

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail
function host-goarch() {
case "$(uname -m)" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
*)
echo ""
;;
esac
}
function require-command() {
local command_name=$1
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "missing required command: $command_name"
return 1
fi
}
function cpu_count() {
if command -v nproc >/dev/null 2>&1; then
nproc
else
getconf _NPROCESSORS_ONLN
fi
}
function download() {
local url=$1
local output=$2
if [ ! -f "$output" ]; then
curl -fsSL "$url" -o "$output"
fi
}
function prepare-dir() {
local dir=$1
rm -rf "$dir"
mkdir -p "$dir"
}
function copy-tree() {
local from_dir=$1
local to_dir=$2
rm -rf "$to_dir"
mkdir -p "$to_dir"
cp -a "$from_dir"/. "$to_dir"/
}
function copy-static-lib() {
local pattern=$1
local destination=$2
local source_path=""
source_path=$(find "$(dirname "$pattern")" -type f -name "$(basename "$pattern")" | head -n 1)
if [ -z "$source_path" ]; then
echo "missing static library: $pattern"
return 1
fi
cp "$source_path" "$destination"
}
ROOT=$(cd "$(dirname "$0")/.." && pwd)
ARCH=${1:-$(host-goarch)}
HOST_ARCH=$(host-goarch)
LIBPCAP_VERSION=${LIBPCAP_VERSION:-1.10.5}
BROTLI_VERSION=${BROTLI_VERSION:-1.1.0}
WORKDIR=${WORKDIR:-"$ROOT/.third_party_build"}
DOWNLOAD_DIR="$WORKDIR/downloads"
SOURCE_DIR="$WORKDIR/sources"
BUILD_DIR="$WORKDIR/build"
LIBPCAP_URL="https://www.tcpdump.org/release/libpcap-${LIBPCAP_VERSION}.tar.gz"
BROTLI_URL="https://github.com/google/brotli/archive/refs/tags/v${BROTLI_VERSION}.tar.gz"
if [ -z "$ARCH" ]; then
echo "unsupported host architecture: $(uname -m)"
echo "usage: $0 [amd64|arm64]"
exit 1
fi
if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ]; then
echo "unsupported architecture: $ARCH"
echo "supported architectures: amd64 arm64"
exit 1
fi
if [ -n "$HOST_ARCH" ] && [ "$ARCH" != "$HOST_ARCH" ]; then
echo "this helper only builds native Ubuntu 22.04 static libraries"
echo "host arch: ${HOST_ARCH}, requested arch: ${ARCH}"
exit 1
fi
for tool in curl tar make cmake gcc g++ ar ranlib; do
require-command "$tool"
done
mkdir -p "$DOWNLOAD_DIR" "$SOURCE_DIR" "$BUILD_DIR"
echo "building third-party static libraries for ${ARCH} ..."
echo "workspace: $WORKDIR"
LIBPCAP_ARCHIVE="$DOWNLOAD_DIR/libpcap-${LIBPCAP_VERSION}.tar.gz"
BROTLI_ARCHIVE="$DOWNLOAD_DIR/brotli-${BROTLI_VERSION}.tar.gz"
download "$LIBPCAP_URL" "$LIBPCAP_ARCHIVE"
download "$BROTLI_URL" "$BROTLI_ARCHIVE"
LIBPCAP_SOURCE="$SOURCE_DIR/libpcap-${LIBPCAP_VERSION}"
BROTLI_SOURCE="$SOURCE_DIR/brotli-${BROTLI_VERSION}"
prepare-dir "$LIBPCAP_SOURCE"
prepare-dir "$BROTLI_SOURCE"
tar -xzf "$LIBPCAP_ARCHIVE" -C "$LIBPCAP_SOURCE" --strip-components=1
tar -xzf "$BROTLI_ARCHIVE" -C "$BROTLI_SOURCE" --strip-components=1
LIBPCAP_BUILD="$BUILD_DIR/libpcap-${ARCH}"
BROTLI_BUILD_SOURCE="$BUILD_DIR/brotli-source-${ARCH}"
BROTLI_BUILD_DIR="$BUILD_DIR/brotli-build-${ARCH}"
prepare-dir "$LIBPCAP_BUILD"
prepare-dir "$BROTLI_BUILD_SOURCE"
prepare-dir "$BROTLI_BUILD_DIR"
cp -a "$LIBPCAP_SOURCE"/. "$LIBPCAP_BUILD"/
cp -a "$BROTLI_SOURCE"/. "$BROTLI_BUILD_SOURCE"/
pushd "$LIBPCAP_BUILD" >/dev/null
./configure \
--disable-shared \
--enable-static \
--without-dbus \
--without-libnl \
--without-bluetooth \
--without-dag \
--without-septel \
--without-snf \
--without-turbocap \
--without-rdma \
--without-netmap \
CFLAGS="-O2 -fPIC"
make -j"$(cpu_count)"
popd >/dev/null
cmake -S "$BROTLI_BUILD_SOURCE" -B "$BROTLI_BUILD_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DBROTLI_DISABLE_TESTS=ON \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
cmake --build "$BROTLI_BUILD_DIR" --parallel "$(cpu_count)"
mkdir -p "$ROOT/libs/libpcap/${ARCH}"
mkdir -p "$ROOT/libs/libbrotli/${ARCH}"
mkdir -p "$ROOT/libs/libpcap/src"
mkdir -p "$ROOT/libs/libbrotli/src"
cp "$LIBPCAP_BUILD/libpcap.a" "$ROOT/libs/libpcap/${ARCH}/libpcap.a"
copy-tree "$LIBPCAP_BUILD" "$ROOT/libs/libpcap/src/libpcap"
copy-static-lib "$BROTLI_BUILD_DIR/libbrotlicommon*.a" "$ROOT/libs/libbrotli/${ARCH}/libbrotlicommon.a"
copy-static-lib "$BROTLI_BUILD_DIR/libbrotlidec*.a" "$ROOT/libs/libbrotli/${ARCH}/libbrotlidec.a"
copy-static-lib "$BROTLI_BUILD_DIR/libbrotlienc*.a" "$ROOT/libs/libbrotli/${ARCH}/libbrotlienc.a"
copy-tree "$BROTLI_SOURCE" "$ROOT/libs/libbrotli/src/brotli"
echo "done"
echo "generated:"
echo " $ROOT/libs/libpcap/${ARCH}/libpcap.a"
echo " $ROOT/libs/libbrotli/${ARCH}/libbrotlicommon.a"
echo " $ROOT/libs/libbrotli/${ARCH}/libbrotlidec.a"
echo " $ROOT/libs/libbrotli/${ARCH}/libbrotlienc.a"
echo ""
echo "next:"
echo " cd \"$ROOT/build\" && ./build.sh linux ${ARCH} plus"

View File

@@ -1,42 +1,217 @@
#!/usr/bin/env bash #!/usr/bin/env bash
function find-binary() {
local candidate
for candidate in "$@"; do
if [ -z "$candidate" ]; then
continue
fi
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
return 1
}
function host-goarch() {
case "$(uname -m)" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
i386|i486|i586|i686)
echo "386"
;;
mips64)
echo "mips64"
;;
mips64el)
echo "mips64le"
;;
*)
echo ""
;;
esac
}
function find-linux-static-toolchain() {
local arch=$1
local cc_bin=""
local cxx_bin=""
local host_arch
host_arch=$(host-goarch)
case "$arch" in
amd64)
cc_bin=$(find-binary \
"/usr/local/gcc/x86_64-unknown-linux-gnu/bin/x86_64-unknown-linux-gnu-gcc" \
"/usr/local/opt/musl-cross/bin/x86_64-linux-musl-gcc" \
"x86_64-unknown-linux-gnu-gcc" \
"x86_64-linux-musl-gcc" \
"musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/gcc/x86_64-unknown-linux-gnu/bin/x86_64-unknown-linux-gnu-g++" \
"/usr/local/opt/musl-cross/bin/x86_64-linux-musl-g++" \
"x86_64-unknown-linux-gnu-g++" \
"x86_64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "amd64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "amd64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
386)
cc_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/i486-linux-musl-gcc" \
"i486-linux-musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/i486-linux-musl-g++" \
"i486-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "386" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "386" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
arm64)
cc_bin=$(find-binary \
"/usr/local/gcc/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-gcc" \
"/usr/local/opt/musl-cross/bin/aarch64-linux-musl-gcc" \
"aarch64-unknown-linux-gnu-gcc" \
"aarch64-linux-musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/gcc/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-g++" \
"/usr/local/opt/musl-cross/bin/aarch64-linux-musl-g++" \
"aarch64-unknown-linux-gnu-g++" \
"aarch64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "arm64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "arm64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
arm)
cc_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/arm-linux-musleabi-gcc" \
"arm-linux-musleabi-gcc")
cxx_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/arm-linux-musleabi-g++" \
"arm-linux-musleabi-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "arm" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "arm" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
mips64)
cc_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/mips64-linux-musl-gcc" \
"mips64-linux-musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/mips64-linux-musl-g++" \
"mips64-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "mips64" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "mips64" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
mips64le)
cc_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/mips64el-linux-musl-gcc" \
"mips64el-linux-musl-gcc")
cxx_bin=$(find-binary \
"/usr/local/opt/musl-cross/bin/mips64el-linux-musl-g++" \
"mips64el-linux-musl-g++")
if [ -z "$cc_bin" ] && [ "$host_arch" = "mips64le" ]; then
cc_bin=$(find-binary "gcc" "cc" "clang")
fi
if [ -z "$cxx_bin" ] && [ "$host_arch" = "mips64le" ]; then
cxx_bin=$(find-binary "g++" "c++" "clang++")
fi
;;
*)
return 1
;;
esac
if [ -z "$cc_bin" ]; then
return 1
fi
if [ -z "$cxx_bin" ]; then
cxx_bin="$cc_bin"
fi
echo "$cc_bin|$cxx_bin"
return 0
}
function require-packet-static-libs() {
local srcdir=$1
local arch=$2
local missing=0
local file
for file in \
"${srcdir}/libs/libpcap/${arch}/libpcap.a" \
"${srcdir}/libs/libbrotli/${arch}/libbrotlienc.a" \
"${srcdir}/libs/libbrotli/${arch}/libbrotlidec.a" \
"${srcdir}/libs/libbrotli/${arch}/libbrotlicommon.a"; do
if [ ! -f "$file" ]; then
echo "missing required plus packet library: $file"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
return 1
fi
return 0
}
function build() { function build() {
ROOT=$(dirname $0) ROOT=$(dirname "$0")
NAME="edge-node" NAME="edge-node"
VERSION=$(lookup-version "$ROOT"/../internal/const/const.go) VERSION=$(lookup-version "$ROOT"/../internal/const/const.go)
DIST=$ROOT/"../dist/${NAME}" DIST=$ROOT/"../dist/${NAME}"
MUSL_DIR="/usr/local/opt/musl-cross/bin"
SRCDIR=$(realpath "$ROOT/..") SRCDIR=$(realpath "$ROOT/..")
# for macOS users: precompiled gcc can be downloaded from https://github.com/messense/homebrew-macos-cross-toolchains
GCC_X86_64_DIR="/usr/local/gcc/x86_64-unknown-linux-gnu/bin"
GCC_ARM64_DIR="/usr/local/gcc/aarch64-unknown-linux-gnu/bin"
OS=${1} OS=${1}
ARCH=${2} ARCH=${2}
TAG=${3} TAG=${3}
if [ -z "$OS" ]; then if [ -z "$OS" ]; then
echo "usage: build.sh OS ARCH" echo "usage: build.sh OS ARCH"
exit exit 1
fi fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
echo "usage: build.sh OS ARCH" echo "usage: build.sh OS ARCH"
exit exit 1
fi fi
if [ -z "$TAG" ]; then if [ -z "$TAG" ]; then
TAG="community" TAG="community"
fi fi
echo "checking ..." echo "checking ..."
ZIP_PATH=$(which zip) if ! command -v zip >/dev/null 2>&1; then
if [ -z "$ZIP_PATH" ]; then
echo "we need 'zip' command to compress files" echo "we need 'zip' command to compress files"
exit exit 1
fi fi
echo "building v${VERSION}/${OS}/${ARCH}/${TAG} ..." echo "building v${VERSION}/${OS}/${ARCH}/${TAG} ..."
# 生成 zip 文件名时不包含 plus 标记
if [ "${TAG}" = "plus" ]; then if [ "${TAG}" = "plus" ]; then
ZIP="${NAME}-${OS}-${ARCH}-v${VERSION}.zip" ZIP="${NAME}-${OS}-${ARCH}-v${VERSION}.zip"
else else
@@ -63,11 +238,8 @@ function build() {
cp -R "$ROOT"/pages "$DIST"/ cp -R "$ROOT"/pages "$DIST"/
copy_fluent_bit_assets "$ROOT" "$DIST" "$OS" "$ARCH" || exit 1 copy_fluent_bit_assets "$ROOT" "$DIST" "$OS" "$ARCH" || exit 1
# we support TOA on linux only if [ "$OS" == "linux" ] && [ -f "${ROOT}/edge-toa/edge-toa-${ARCH}" ]; then
if [ "$OS" == "linux" ] && [ -f "${ROOT}/edge-toa/edge-toa-${ARCH}" ] if [ ! -d "$DIST/edge-toa" ]; then
then
if [ ! -d "$DIST/edge-toa" ]
then
mkdir "$DIST/edge-toa" mkdir "$DIST/edge-toa"
fi fi
cp "${ROOT}/edge-toa/edge-toa-${ARCH}" "$DIST/edge-toa/edge-toa" cp "${ROOT}/edge-toa/edge-toa-${ARCH}" "$DIST/edge-toa/edge-toa"
@@ -75,96 +247,59 @@ function build() {
echo "building ..." echo "building ..."
CC_PATH="" CC_BIN=""
CXX_PATH="" CXX_BIN=""
CGO_LDFLAGS="" CGO_LDFLAGS=""
CGO_CFLAGS="" CGO_CFLAGS=""
BUILD_TAG=$TAG BUILD_TAG=$TAG
if [[ `uname -a` == *"Darwin"* && "${OS}" == "linux" ]]; then
if [ "${ARCH}" == "amd64" ]; then
# build with script support
if [ -d $GCC_X86_64_DIR ]; then
MUSL_DIR=$GCC_X86_64_DIR
CC_PATH="x86_64-unknown-linux-gnu-gcc"
CXX_PATH="x86_64-unknown-linux-gnu-g++"
if [ "$TAG" = "plus" ]; then
BUILD_TAG="plus,script,packet"
fi
else
CC_PATH="x86_64-linux-musl-gcc"
CXX_PATH="x86_64-linux-musl-g++"
fi
fi
if [ "${ARCH}" == "386" ]; then
CC_PATH="i486-linux-musl-gcc"
CXX_PATH="i486-linux-musl-g++"
fi
if [ "${ARCH}" == "arm64" ]; then
# build with script support
if [ -d $GCC_ARM64_DIR ]; then
MUSL_DIR=$GCC_ARM64_DIR
CC_PATH="aarch64-unknown-linux-gnu-gcc"
CXX_PATH="aarch64-unknown-linux-gnu-g++"
if [ "$TAG" = "plus" ]; then
BUILD_TAG="plus,script,packet"
fi
else
CC_PATH="aarch64-linux-musl-gcc"
CXX_PATH="aarch64-linux-musl-g++"
fi
fi
if [ "${ARCH}" == "arm" ]; then
CC_PATH="arm-linux-musleabi-gcc"
CXX_PATH="arm-linux-musleabi-g++"
fi
if [ "${ARCH}" == "mips64" ]; then
CC_PATH="mips64-linux-musl-gcc"
CXX_PATH="mips64-linux-musl-g++"
fi
if [ "${ARCH}" == "mips64le" ]; then
CC_PATH="mips64el-linux-musl-gcc"
CXX_PATH="mips64el-linux-musl-g++"
fi
fi
# libpcap if [ "$OS" == "linux" ] && [[ "$ARCH" == "amd64" || "$ARCH" == "arm64" ]] && [ "$TAG" == "plus" ]; then
if [ "$OS" == "linux" ] && [[ "$ARCH" == "amd64" || "$ARCH" == "arm64" ]] && [ "$TAG" == "plus" ]; then require-packet-static-libs "$SRCDIR" "$ARCH" || exit 1
BUILD_TAG="plus,script,packet"
CGO_LDFLAGS="-L${SRCDIR}/libs/libpcap/${ARCH} -lpcap -L${SRCDIR}/libs/libbrotli/${ARCH} -lbrotlienc -lbrotlidec -lbrotlicommon" CGO_LDFLAGS="-L${SRCDIR}/libs/libpcap/${ARCH} -lpcap -L${SRCDIR}/libs/libbrotli/${ARCH} -lbrotlienc -lbrotlidec -lbrotlicommon"
CGO_CFLAGS="-I${SRCDIR}/libs/libpcap/src/libpcap -I${SRCDIR}/libs/libpcap/src/libpcap/pcap -I${SRCDIR}/libs/libbrotli/src/brotli/c/include -I${SRCDIR}/libs/libbrotli/src/brotli/c/include/brotli" CGO_CFLAGS="-I${SRCDIR}/libs/libpcap/src/libpcap -I${SRCDIR}/libs/libpcap/src/libpcap/pcap -I${SRCDIR}/libs/libbrotli/src/brotli/c/include -I${SRCDIR}/libs/libbrotli/src/brotli/c/include/brotli"
fi fi
if [ ! -z $CC_PATH ]; then if [ "$OS" == "linux" ]; then
env CC=$MUSL_DIR/$CC_PATH \ TOOLCHAIN=$(find-linux-static-toolchain "$ARCH")
CXX=$MUSL_DIR/$CXX_PATH GOOS="${OS}" \ if [ -z "$TOOLCHAIN" ]; then
GOARCH="${ARCH}" CGO_ENABLED=1 \ echo "could not find a static Linux toolchain for ${ARCH}"
CGO_LDFLAGS="${CGO_LDFLAGS}" \ echo "install a musl cross compiler or provide /usr/local/gcc toolchains before building"
CGO_CFLAGS="${CGO_CFLAGS}" \ exit 1
go build -trimpath -tags $BUILD_TAG -o "$DIST"/bin/${NAME} -ldflags "-linkmode external -extldflags -static -s -w" "$ROOT"/../cmd/edge-node/main.go
else
if [[ `uname` == *"Linux"* ]] && [ "$OS" == "linux" ] && [[ "$ARCH" == "amd64" || "$ARCH" == "arm64" ]] && [ "$TAG" == "plus" ]; then
BUILD_TAG="plus,script,packet"
fi fi
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 CGO_LDFLAGS="${CGO_LDFLAGS}" CGO_CFLAGS="${CGO_CFLAGS}" go build -trimpath -tags $BUILD_TAG -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-node/main.go CC_BIN=${TOOLCHAIN%|*}
CXX_BIN=${TOOLCHAIN#*|}
env CC="$CC_BIN" \
CXX="$CXX_BIN" \
GOOS="${OS}" \
GOARCH="${ARCH}" \
CGO_ENABLED=1 \
CGO_LDFLAGS="${CGO_LDFLAGS}" \
CGO_CFLAGS="${CGO_CFLAGS}" \
go build -trimpath -tags "$BUILD_TAG" -o "$DIST"/bin/${NAME} -ldflags "-linkmode external -extldflags -static -s -w" "$ROOT"/../cmd/edge-node/main.go
else
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 CGO_LDFLAGS="${CGO_LDFLAGS}" CGO_CFLAGS="${CGO_CFLAGS}" \
go build -trimpath -tags "$BUILD_TAG" -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-node/main.go
fi fi
if [ ! -f "${DIST}/bin/${NAME}" ]; then if [ ! -f "${DIST}/bin/${NAME}" ]; then
echo "build failed!" echo "build failed!"
exit exit 1
fi fi
# delete hidden files
find "$DIST" -name ".DS_Store" -delete find "$DIST" -name ".DS_Store" -delete
find "$DIST" -name ".gitignore" -delete find "$DIST" -name ".gitignore" -delete
echo "zip files" echo "zip files"
cd "${DIST}/../" || exit cd "${DIST}/../" || exit 1
if [ -f "${ZIP}" ]; then if [ -f "${ZIP}" ]; then
rm -f "${ZIP}" rm -f "${ZIP}"
fi fi
zip -r -X -q "${ZIP}" ${NAME}/ zip -r -X -q "${ZIP}" ${NAME}/
rm -rf ${NAME} rm -rf ${NAME}
cd - || exit cd - || exit 1
echo "OK" echo "OK"
} }
@@ -253,7 +388,7 @@ function lookup-version() {
echo "$VERSION" echo "$VERSION"
else else
echo "could not match version" echo "could not match version"
exit exit 1
fi fi
} }

View File

@@ -1 +1 @@
{"speed":1,"speedMB":1400,"countTests":3} {"speed":1,"speedMB":1510,"countTests":10}

View File

@@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
sudo go run -tags="plus script packet" ../cmd/edge-node/main.go sudo go run -tags="plus script" ../cmd/edge-node/main.go

View File

@@ -20,7 +20,6 @@ require (
github.com/dchest/captcha v0.0.0-00010101000000-000000000000 github.com/dchest/captcha v0.0.0-00010101000000-000000000000
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.2.0 github.com/google/nftables v0.2.0
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6 github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6

View File

@@ -67,8 +67,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8=
github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=

View File

@@ -1,7 +1,7 @@
package teaconst package teaconst
const ( const (
Version = "1.4.9" //1.3.8.2 Version = "1.5.0" //1.3.8.2
ProductName = "Edge Node" ProductName = "Edge Node"
ProcessName = "edge-node" ProcessName = "edge-node"

View File

@@ -1,135 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package networksecurity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/events"
"github.com/TeaOSLab/EdgeNode/internal/monitor"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
"github.com/TeaOSLab/EdgeNode/internal/utils/netpackets"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/maps"
"runtime"
"time"
)
var SharedManager = NewManager()
func init() {
if !teaconst.IsMain {
return
}
events.On(events.EventLoaded, func() {
nodeConfig, _ := nodeconfigs.SharedNodeConfig()
if nodeConfig != nil {
go SharedManager.Apply(nodeConfig.NetworkSecurityPolicy)
}
})
events.On(events.EventQuit, func() {
go SharedManager.Apply(nil)
})
goman.New(func() {
var ticker = time.NewTicker(1 * time.Minute)
for range ticker.C {
SharedManager.Upload()
}
})
}
type Manager struct {
listener *netpackets.Listener
isRunning bool
policy *nodeconfigs.NetworkSecurityPolicy
totalTCPPacketsMinutely uint64
totalUDPPacketsMinutely uint64
totalICMPPacketsMinutely uint64
}
func NewManager() *Manager {
return &Manager{}
}
// Apply 应用配置
// 非线程安全
func (this *Manager) Apply(policy *nodeconfigs.NetworkSecurityPolicy) {
if this.policy != nil && this.policy.IsSame(policy) {
return
}
this.policy = policy
if policy == nil ||
policy.Status == nodeconfigs.NetworkSecurityStatusOff ||
(policy.Status == nodeconfigs.NetworkSecurityStatusAuto && runtime.NumCPU() < 8) {
if this.listener != nil {
remotelogs.Println("NETWORK_SECURITY_MANAGER", "stop")
this.listener.Stop()
}
this.isRunning = false
return
}
if this.listener == nil {
this.listener = netpackets.NewListener()
// References:
// - https://biot.com/capstats/bpf.html
// - https://www.ibm.com/docs/en/qsip/7.4?topic=queries-berkeley-packet-filters
// - https://www.tcpdump.org/manpages/tcpdump.1.html
if Tea.IsTesting() || utils.IsDebugEnv() { // dev environment
this.listener.SetBPF("(tcp or udp or icmp) and not net 127 and not net ::1")
} else {
this.listener.SetBPF("(tcp or udp or icmp) and not src net 127 and not src net 192.168 and not src net 172.16 and not src net ::1 and not src net 10")
}
this.listener.AddFilter(this)
}
if !this.isRunning {
this.isRunning = true
remotelogs.Println("NETWORK_SECURITY_MANAGER", "start")
err := this.listener.Start() // long run function
if err != nil {
remotelogs.Error("NETWORK_SECURITY_MANAGER", "start listener failed: "+err.Error())
}
this.isRunning = false
}
}
func (this *Manager) FilterMeta(meta *netpackets.PacketMeta) {
switch meta.LayerType {
case netpackets.LayerTypeTCP:
// 这里不需要试用atomic因为数据不需要那么精确
this.totalTCPPacketsMinutely++
case netpackets.LayerTypeUDP:
this.totalUDPPacketsMinutely++
case netpackets.LayerTypeICMPv4, netpackets.LayerTypeICMPv6:
this.totalICMPPacketsMinutely++
}
}
func (this *Manager) Upload() {
if !this.isRunning {
return
}
monitor.SharedValueQueue.Add(nodeconfigs.NodeValueItemNetworkPackets, maps.Map{
"tcpInPPS": this.totalTCPPacketsMinutely / 60,
"udpInPPS": this.totalUDPPacketsMinutely / 60,
"icmpInPPS": this.totalICMPPacketsMinutely / 60,
})
this.totalTCPPacketsMinutely = 0
this.totalUDPPacketsMinutely = 0
this.totalICMPPacketsMinutely = 0
}

View File

@@ -1,24 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package networksecurity_test
import (
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
networksecurity "github.com/TeaOSLab/EdgeNode/internal/network-security"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"os"
"testing"
)
func TestManager_Apply(t *testing.T) {
if !testutils.IsSingleTesting() {
if os.Getgid() > 0 {
return
}
}
var manager = networksecurity.NewManager()
var policy = nodeconfigs.NewNetworkSecurityPolicy()
manager.Apply(policy)
}

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env bash
sudo go test -v -tags="plus packet" -run '^TestManager_Apply'

View File

@@ -9,14 +9,20 @@ import (
"net/http" "net/http"
) )
// 执行认证 // 鎵ц璁よ瘉
func (this *HTTPRequest) doAuth() (shouldStop bool) { func (this *HTTPRequest) doAuth() (shouldStop bool) {
if this.web.Auth == nil || !this.web.Auth.IsOn { if this.web.Auth == nil || !this.web.Auth.IsOn {
return return
} }
for _, ref := range this.web.Auth.PolicyRefs { for _, ref := range this.web.Auth.PolicyRefs {
if !ref.IsOn || ref.AuthPolicy == nil || !ref.AuthPolicy.IsOn { if !ref.IsOn {
continue
}
if ref.AuthPolicy == nil {
continue
}
if !ref.AuthPolicy.IsOn {
continue continue
} }
if !ref.AuthPolicy.MatchRequest(this.RawReq) { if !ref.AuthPolicy.MatchRequest(this.RawReq) {
@@ -36,7 +42,7 @@ func (this *HTTPRequest) doAuth() (shouldStop bool) {
return writer.StatusCode(), nil return writer.StatusCode(), nil
}, this.Format) }, this.Format)
if err != nil { if err != nil {
this.write50x(err, http.StatusInternalServerError, "Failed to execute the AuthPolicy", "认证策略执行失败", false) this.write50x(err, http.StatusInternalServerError, "Failed to execute the AuthPolicy", "璁よ瘉绛栫暐鎵ц澶辫触", false)
return return
} }
if ok { if ok {
@@ -45,28 +51,28 @@ func (this *HTTPRequest) doAuth() (shouldStop bool) {
} }
this.tags = append(this.tags, "auth:"+ref.AuthPolicy.Type) this.tags = append(this.tags, "auth:"+ref.AuthPolicy.Type)
return return
} else {
// Basic Auth比较特殊
if ref.AuthPolicy.Type == serverconfigs.HTTPAuthTypeBasicAuth {
method, ok := ref.AuthPolicy.Method().(*serverconfigs.HTTPAuthBasicMethod)
if ok {
var headerValue = "Basic realm=\""
if len(method.Realm) > 0 {
headerValue += method.Realm
} else {
headerValue += this.ReqHost
}
headerValue += "\""
if len(method.Charset) > 0 {
headerValue += ", charset=\"" + method.Charset + "\""
}
this.writer.Header()["WWW-Authenticate"] = []string{headerValue}
}
}
this.writer.WriteHeader(http.StatusUnauthorized)
this.tags = append(this.tags, "auth:"+ref.AuthPolicy.Type)
return true
} }
// Basic Auth 姣旇緝鐗规畩
if ref.AuthPolicy.Type == serverconfigs.HTTPAuthTypeBasicAuth {
method, ok := ref.AuthPolicy.Method().(*serverconfigs.HTTPAuthBasicMethod)
if ok {
var headerValue = "Basic realm=\""
if len(method.Realm) > 0 {
headerValue += method.Realm
} else {
headerValue += this.ReqHost
}
headerValue += "\""
if len(method.Charset) > 0 {
headerValue += ", charset=\"" + method.Charset + "\""
}
this.writer.Header()["WWW-Authenticate"] = []string{headerValue}
}
}
this.writer.WriteHeader(http.StatusUnauthorized)
this.tags = append(this.tags, "auth:"+ref.AuthPolicy.Type)
return true
} }
return return
} }

View File

@@ -1,35 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package nodes
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
networksecurity "github.com/TeaOSLab/EdgeNode/internal/network-security"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/rpc"
)
func (this *Node) execNetworkSecurityPolicyChangedTask(rpcClient *rpc.RPCClient) error {
remotelogs.Println("NODE", "updating network security policy ...")
resp, err := rpcClient.NodeRPC.FindNodeNetworkSecurityPolicy(rpcClient.Context(), &pb.FindNodeNetworkSecurityPolicyRequest{})
if err != nil {
return err
}
var policy = nodeconfigs.NewNetworkSecurityPolicy()
if len(resp.NetworkSecurityPolicyJSON) > 0 {
err = json.Unmarshal(resp.NetworkSecurityPolicyJSON, policy)
if err != nil {
return err
}
}
sharedNodeConfig.NetworkSecurityPolicy = policy
go networksecurity.SharedManager.Apply(policy)
return nil
}

View File

@@ -1,5 +1,5 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . // Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && !packet //go:build plus
package nodes package nodes

View File

@@ -1,8 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package netpackets
type FilterInterface interface {
FilterMeta(meta *PacketMeta)
}

View File

@@ -1,92 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package netpackets
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/linkedlist"
"sync"
)
type IgnoredIPList struct {
mu sync.RWMutex
itemMap map[string]*linkedlist.Item[string] // linked => item
list *linkedlist.List[string]
capacity int
lastIP string
}
func NewIgnoredIPList(capacity int) *IgnoredIPList {
return &IgnoredIPList{
itemMap: map[string]*linkedlist.Item[string]{},
list: linkedlist.NewList[string](),
capacity: capacity,
}
}
func (this *IgnoredIPList) Add(ip string) {
this.mu.Lock()
defer this.mu.Unlock()
if this.lastIP == ip {
return
}
this.lastIP = ip
item, ok := this.itemMap[ip]
if !ok {
if this.capacity > 0 && len(this.itemMap) == this.capacity {
var firstItem = this.list.Shift()
if firstItem != nil {
delete(this.itemMap, firstItem.Value)
}
}
item = linkedlist.NewItem[string](ip)
this.itemMap[ip] = item
}
this.list.Push(item)
}
func (this *IgnoredIPList) Remove(ip string) {
this.mu.Lock()
defer this.mu.Unlock()
item, ok := this.itemMap[ip]
if ok {
delete(this.itemMap, ip)
this.list.Remove(item)
}
}
func (this *IgnoredIPList) Contains(ip string) bool {
this.mu.RLock()
defer this.mu.RUnlock()
_, ok := this.itemMap[ip]
return ok
}
func (this *IgnoredIPList) List(size int) (ipList []string) {
if size <= 0 {
return
}
this.mu.RLock()
defer this.mu.RUnlock()
this.list.RangeReverse(func(item *linkedlist.Item[string]) (goNext bool) {
ipList = append(ipList, item.Value)
size--
return size > 0
})
return
}
func (this *IgnoredIPList) Len() int {
this.mu.RLock()
defer this.mu.RUnlock()
return len(this.itemMap)
}

View File

@@ -1,71 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package netpackets_test
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/netpackets"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/rands"
"runtime"
"testing"
)
func TestIgnoredIPList_Add(t *testing.T) {
var a = assert.NewAssertion(t)
var list = netpackets.NewIgnoredIPList(10)
list.Add("192.168.2.1")
list.Add("192.168.2.2")
list.Add("192.168.2.3")
a.IsTrue(list.Contains("192.168.2.1"))
a.IsFalse(list.Contains("192.168.2.0"))
t.Log(list.List(0))
t.Log(list.List(2))
t.Log(list.List(4))
}
func TestIgnoredIPList_Add_Capacity(t *testing.T) {
var list = netpackets.NewIgnoredIPList(4)
list.Add("192.168.2.1")
list.Add("192.168.2.2")
list.Add("192.168.2.3")
list.Add("192.168.2.4")
list.Add("192.168.2.5")
list.Add("192.168.2.6")
list.Add("192.168.2.7")
t.Log(list.List(10))
t.Log(list.Len(), "items")
}
func TestIgnoredIPList_Remove(t *testing.T) {
var list = netpackets.NewIgnoredIPList(10)
list.Add("192.168.2.1")
list.Add("192.168.2.2")
list.Add("192.168.2.3")
list.Remove("192.168.2.2")
t.Log(list.List(4))
}
func BenchmarkIgnoredIPList_Add(b *testing.B) {
runtime.GOMAXPROCS(1)
var genIPFunc = func() string {
return fmt.Sprintf("%d.%d.%d.%d", rands.Int(0, 255), rands.Int(0, 255), rands.Int(0, 255), rands.Int(0, 255))
}
var list = netpackets.NewIgnoredIPList(65535)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
list.Add(genIPFunc())
list.Remove(genIPFunc())
list.Contains(genIPFunc())
if rands.Int(0, 100) == 0 {
list.List(1000)
}
}
})
}

View File

@@ -1,18 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package netpackets
import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
type LayerType = gopacket.LayerType
var (
LayerTypeTCP = layers.LayerTypeTCP
LayerTypeUDP = layers.LayerTypeUDP
LayerTypeICMPv4 = layers.LayerTypeICMPv4
LayerTypeICMPv6 = layers.LayerTypeICMPv6
)

View File

@@ -1,293 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package netpackets
import (
"encoding/binary"
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"sort"
"strings"
"time"
)
const defaultBPFFilter = "(tcp or udp or icmp) and not net 127.0.0.1"
type Listener struct {
filters []FilterInterface
incomingHandle *pcap.Handle
bpfFilter string
decodeDstIP bool
isClosed bool
outgoingIPList *IgnoredIPList
outgoingHandle *pcap.Handle
filterTicker *time.Ticker
}
func NewListener() *Listener {
return &Listener{
isClosed: true,
outgoingIPList: NewIgnoredIPList(65535),
}
}
func (this *Listener) Start() error {
if !this.isClosed {
return nil
}
this.isClosed = false
go func() {
startErr := this.loopOutgoing()
if startErr != nil {
remotelogs.Error("NET_PACKET", "start outgoing packet listener failed: "+startErr.Error())
}
}()
go func() {
this.loopUpdateFilter()
}()
for { // 无限 for 是为了防止意外退出
err := this.loopIncoming()
if err != nil {
if this.isClosed {
return nil
}
return fmt.Errorf("start packet listener failed: %w", err)
}
if this.isClosed {
return nil
}
}
}
func (this *Listener) AddFilter(filter FilterInterface) {
this.filters = append(this.filters, filter)
}
func (this *Listener) SetBPF(bpfFilter string) {
this.bpfFilter = bpfFilter
}
func (this *Listener) DecodeDstIP() {
this.decodeDstIP = true
}
func (this *Listener) Stop() {
this.isClosed = true
this.incomingHandle.Close()
this.outgoingHandle.Close()
}
func (this *Listener) IsRunning() bool {
return this.incomingHandle != nil && !this.isClosed
}
func (this *Listener) loopIncoming() error {
const device = "any"
var err error
this.incomingHandle, err = pcap.OpenLive(device, 128, false /** ignore collision domain **/, pcap.BlockForever)
if err != nil {
return err
}
defer func() {
this.incomingHandle.Close()
}()
err = this.incomingHandle.SetDirection(pcap.DirectionIn)
if err != nil {
return err
}
if len(this.bpfFilter) > 0 {
err = this.incomingHandle.SetBPFFilter(this.bpfFilter)
} else {
this.bpfFilter = defaultBPFFilter
err = this.incomingHandle.SetBPFFilter(defaultBPFFilter)
}
if err != nil {
return err
}
var meta = &PacketMeta{}
var packetSource = gopacket.NewPacketSource(this.incomingHandle, this.incomingHandle.LinkType())
packetSource.NoCopy = true
packetSource.Lazy = true
var filters = this.filters
var countFilters = len(filters)
for packet := range packetSource.Packets() {
var networkLayer = packet.NetworkLayer()
if networkLayer == nil {
continue
}
var networkFlow = networkLayer.NetworkFlow()
var src = networkFlow.Src()
meta.SrcIP = src.String()
// ignore outgoing ip
if this.outgoingIPList.Contains(meta.SrcIP) {
continue
}
if this.decodeDstIP {
meta.DstIP = networkFlow.Dst().String()
}
meta.Length = packet.Metadata().Length
var transportLayer = packet.TransportLayer()
if transportLayer == nil {
meta.SrcPort = 0
meta.DstPort = 0
switch x := networkLayer.(type) {
case *layers.IPv4:
meta.LayerType = x.NextLayerType()
case *layers.IPv6:
meta.LayerType = x.NextLayerType()
}
// call filters
if countFilters == 1 {
filters[0].FilterMeta(meta)
} else {
for _, filter := range filters {
filter.FilterMeta(meta)
}
}
continue
}
var transportFlow = transportLayer.TransportFlow()
meta.SrcPort = int(binary.BigEndian.Uint16(transportFlow.Src().Raw()))
meta.LayerType = transportLayer.LayerType()
meta.DstPort = int(binary.BigEndian.Uint16(transportFlow.Dst().Raw()))
// call filters
if countFilters == 1 {
filters[0].FilterMeta(meta)
} else {
for _, filter := range filters {
filter.FilterMeta(meta)
}
}
}
return nil
}
func (this *Listener) loopOutgoing() error {
const device = "any"
var err error
this.outgoingHandle, err = pcap.OpenLive(device, 128, true /** ignore collision domain **/, pcap.BlockForever)
if err != nil {
return err
}
defer func() {
this.outgoingHandle.Close()
}()
err = this.outgoingHandle.SetDirection(pcap.DirectionOut)
if err != nil {
return err
}
err = this.outgoingHandle.SetBPFFilter("tcp and tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) = 0")
if err != nil {
return err
}
var packetSource = gopacket.NewPacketSource(this.outgoingHandle, this.outgoingHandle.LinkType())
packetSource.NoCopy = true
packetSource.Lazy = true
for packet := range packetSource.Packets() {
var networkLayer = packet.NetworkLayer()
if networkLayer == nil {
continue
}
var networkFlow = networkLayer.NetworkFlow()
var dstIP = networkFlow.Dst().String()
this.outgoingIPList.Add(dstIP)
}
return nil
}
func (this *Listener) loopUpdateFilter() {
if this.filterTicker != nil {
return
}
this.filterTicker = time.NewTicker(1 * time.Second)
var lastIPList []string
for range this.filterTicker.C {
if this.isClosed {
continue
}
var ipList = this.outgoingIPList.List(512) // 基于bfp长度的限制这里数量不能太多
sort.Strings(ipList)
if this.equalStrings(lastIPList, ipList) {
continue
}
lastIPList = ipList
// apply
var incomingHandle = this.incomingHandle
if incomingHandle != nil {
var rules = []string{}
for _, ip := range ipList {
rules = append(rules, "not src host "+ip)
}
var newBPFFilter = this.bpfFilter + " and " + strings.Join(rules, " and ")
if utils.IsDebugEnv() {
remotelogs.Debug("NET_PACKET", "set new BPF filter: "+newBPFFilter)
}
err := incomingHandle.SetBPFFilter(newBPFFilter)
if err != nil {
remotelogs.Error("NET_PACKET", "set new BPF filter failed: "+err.Error())
}
}
}
}
func (this *Listener) equalStrings(s1 []string, s2 []string) bool {
var l = len(s1)
if len(s2) != l {
return false
}
if l == 0 {
return true
}
for i := 0; i < l; i++ {
if s1[i] != s2[i] {
return false
}
}
return true
}

View File

@@ -1,105 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package netpackets_test
import (
"encoding/binary"
"github.com/TeaOSLab/EdgeNode/internal/utils/netpackets"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/iwind/TeaGo/types"
"log"
"os"
"testing"
"time"
)
type testFilter struct {
}
func (this *testFilter) FilterMeta(meta *netpackets.PacketMeta) {
log.Println(meta.LayerType.String() + " " + meta.SrcIP + ":" + types.String(meta.SrcPort) + " => " + meta.DstIP + ":" + types.String(meta.DstPort) + " " + types.String(meta.Length) + "bytes")
}
func TestListener_Start(t *testing.T) {
if !testutils.IsSingleTesting() {
if os.Getgid() > 0 {
return
}
}
var listener = netpackets.NewListener()
listener.AddFilter(&testFilter{})
go func() {
time.Sleep(10 * time.Second)
t.Log("stopping ...")
listener.Stop()
}()
t.Log("starting ...")
err := listener.Start()
if err != nil {
t.Fatal(err)
}
}
func TestListener_DecodePacket_UDP(t *testing.T) {
var packetData = []byte{69, 0, 0, 134, 140, 133, 0, 0, 118, 17, 16, 79, 223, 5, 5, 5, 192, 168, 2, 224, 0, 53, 232, 163, 0, 114, 0, 0, 69, 42, 129, 128, 0, 1, 0, 3, 0, 0, 0, 0, 6, 115, 116, 97, 116, 105, 99, 7, 111, 115, 99, 104, 105, 110, 97, 3, 110, 101, 116, 0, 0, 1, 0, 1, 192, 12, 0, 5, 0, 1, 0, 0, 0, 1, 0, 25, 10, 115, 116, 97, 116, 105, 99, 45, 111, 115, 99, 2, 98, 48, 5, 97, 105, 99, 100, 110, 3, 99, 111, 109, 0, 192, 48, 0, 5, 0, 1, 0, 0, 0, 1, 0, 5, 2, 118, 109, 192, 62, 192, 85, 0, 1, 0, 1, 0, 0, 0, 1, 0, 4, 218, 28, 104, 157}
var packet = gopacket.NewPacket(packetData, layers.LayerTypeIPv4, gopacket.DecodeOptions{})
var networkFlow = packet.NetworkLayer().NetworkFlow()
t.Log(networkFlow)
t.Log(packet.Metadata().Length)
t.Log(packet.TransportLayer().TransportFlow())
}
func TestListener_DecodePacket_TCP(t *testing.T) {
var packetData = []byte{69, 8, 0, 64, 6, 51, 64, 0, 52, 6, 188, 222, 74, 91, 117, 187, 192, 168, 2, 224, 1, 187, 225, 226, 137, 198, 251, 25, 221, 137, 133, 93, 176, 16, 1, 245, 224, 6, 0, 0, 1, 1, 8, 10, 30, 187, 162, 175, 35, 215, 100, 174, 1, 1, 5, 10, 221, 137, 133, 68, 221, 137, 133, 93}
var packet = gopacket.NewPacket(packetData, layers.LayerTypeIPv4, gopacket.DecodeOptions{})
var networkFlow = packet.NetworkLayer().NetworkFlow()
t.Log(networkFlow.Src().Raw(), len(networkFlow.Src().Raw()))
t.Log(networkFlow)
t.Log(packet.Metadata().Length)
t.Log(packet.TransportLayer().TransportFlow())
}
func BenchmarkListener_DecodePacket(b *testing.B) {
var packetData = []byte{69, 0, 0, 134, 140, 133, 0, 0, 118, 17, 16, 79, 223, 5, 5, 5, 192, 168, 2, 224, 0, 53, 232, 163, 0, 114, 0, 0, 69, 42, 129, 128, 0, 1, 0, 3, 0, 0, 0, 0, 6, 115, 116, 97, 116, 105, 99, 7, 111, 115, 99, 104, 105, 110, 97, 3, 110, 101, 116, 0, 0, 1, 0, 1, 192, 12, 0, 5, 0, 1, 0, 0, 0, 1, 0, 25, 10, 115, 116, 97, 116, 105, 99, 45, 111, 115, 99, 2, 98, 48, 5, 97, 105, 99, 100, 110, 3, 99, 111, 109, 0, 192, 48, 0, 5, 0, 1, 0, 0, 0, 1, 0, 5, 2, 118, 109, 192, 62, 192, 85, 0, 1, 0, 1, 0, 0, 0, 1, 0, 4, 218, 28, 104, 157}
var decodeOptions = gopacket.DecodeOptions{
Lazy: true,
NoCopy: true,
//SkipDecodeRecovery: true,
}
for i := 0; i < b.N; i++ {
var packet = gopacket.NewPacket(packetData, layers.LayerTypeIPv4, decodeOptions)
var networkFlow = packet.NetworkLayer().NetworkFlow()
var src = networkFlow.Src()
var dest = networkFlow.Dst()
_ = netpackets.IsLocalRawIPv4(src.Raw())
_ = netpackets.IsLocalRawIPv4(dest.Raw())
_ = src.String()
_ = dest.String()
_ = packet.Metadata().Length
var transportFlow = packet.TransportLayer().TransportFlow()
//_ = transportFlow.Src().String()
//_ = transportFlow.Dst().String()
_ = int(binary.BigEndian.Uint16(transportFlow.Src().Raw()))
_ = int(binary.BigEndian.Uint16(transportFlow.Dst().Raw()))
}
}

View File

@@ -1,13 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus && packet
package netpackets
type PacketMeta struct {
LayerType LayerType
SrcIP string
SrcPort int
DstIP string
DstPort int
Length int
}

View File

@@ -1,18 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package netpackets
// IsLocalRawIPv4 使用原始IP数据判断是否为本地IPv4
func IsLocalRawIPv4(ip []byte) bool {
if len(ip) != 4 {
return false
}
if ip[0] == 127 ||
ip[0] == 10 ||
(ip[0] == 172 && ip[1]&0xf0 == 16) ||
(ip[0] == 192 && ip[1] == 168) {
return true
}
return false
}

View File

@@ -1,21 +0,0 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package netpackets_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/netpackets"
"github.com/iwind/TeaGo/assert"
"net"
"testing"
)
func TestIsLocalRawIPv4(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsTrue(netpackets.IsLocalRawIPv4(net.ParseIP("192.168.2.100").To4()))
a.IsTrue(netpackets.IsLocalRawIPv4(net.ParseIP("127.0.0.1").To4()))
a.IsTrue(netpackets.IsLocalRawIPv4(net.ParseIP("172.16.0.1").To4()))
a.IsTrue(netpackets.IsLocalRawIPv4(net.ParseIP("10.0.0.1").To4()))
a.IsFalse(netpackets.IsLocalRawIPv4(net.ParseIP("1.2.3.4").To4()))
}

View File

@@ -1,7 +1,7 @@
package teaconst package teaconst
const ( const (
Version = "0.1.5" Version = "1.5.0"
ProductName = "Edge Reporter" ProductName = "Edge Reporter"
ProcessName = "edge-reporter" ProcessName = "edge-reporter"

View File

@@ -1,7 +1,7 @@
package teaconst package teaconst
const ( const (
Version = "1.4.9" //1.3.8.2 Version = "1.5.0" //1.3.8.2
ProductName = "Edge User" ProductName = "Edge User"
ProcessName = "edge-user" ProcessName = "edge-user"

View File

@@ -174,6 +174,8 @@ replace_files() {
# 替换 EdgeAdmin bin # 替换 EdgeAdmin bin
if [ -d "$TEMP_DIR/edge-admin/bin" ]; then if [ -d "$TEMP_DIR/edge-admin/bin" ]; then
log_info "替换 EdgeAdmin 可执行文件..." log_info "替换 EdgeAdmin 可执行文件..."
rm -rf "$TARGET_DIR/bin"
mkdir -p "$TARGET_DIR/bin"
cp -r "$TEMP_DIR/edge-admin/bin"/* "$TARGET_DIR/bin/" cp -r "$TEMP_DIR/edge-admin/bin"/* "$TARGET_DIR/bin/"
log_info "✅ EdgeAdmin bin 已更新" log_info "✅ EdgeAdmin bin 已更新"
fi fi
@@ -181,15 +183,9 @@ replace_files() {
# 替换 EdgeAdmin web排除 tmp # 替换 EdgeAdmin web排除 tmp
if [ -d "$TEMP_DIR/edge-admin/web" ]; then if [ -d "$TEMP_DIR/edge-admin/web" ]; then
log_info "替换 EdgeAdmin 前端文件..." log_info "替换 EdgeAdmin 前端文件..."
if command -v rsync > /dev/null; then rm -rf "$TARGET_DIR/web"
rsync -av --exclude='tmp' \ cp -r "$TEMP_DIR/edge-admin/web" "$TARGET_DIR/"
"$TEMP_DIR/edge-admin/web/" "$TARGET_DIR/web/" mkdir -p "$TARGET_DIR/web/tmp"
else
# 如果没有 rsync使用 cp
cp -r "$TEMP_DIR/edge-admin/web"/* "$TARGET_DIR/web/" 2>/dev/null || true
rm -rf "$TARGET_DIR/web/tmp"/* 2>/dev/null || true
fi
# 清空 tmp 目录
rm -rf "$TARGET_DIR/web/tmp"/* 2>/dev/null || true rm -rf "$TARGET_DIR/web/tmp"/* 2>/dev/null || true
log_info "✅ EdgeAdmin web 已更新" log_info "✅ EdgeAdmin web 已更新"
fi fi
@@ -203,6 +199,7 @@ replace_files() {
# 替换 bin # 替换 bin
if [ -d "$TEMP_DIR/edge-admin/edge-api/bin" ]; then if [ -d "$TEMP_DIR/edge-admin/edge-api/bin" ]; then
rm -rf "$TARGET_DIR/edge-api/bin"
mkdir -p "$TARGET_DIR/edge-api/bin" mkdir -p "$TARGET_DIR/edge-api/bin"
cp -r "$TEMP_DIR/edge-admin/edge-api/bin"/* \ cp -r "$TEMP_DIR/edge-admin/edge-api/bin"/* \
"$TARGET_DIR/edge-api/bin/" 2>/dev/null || true "$TARGET_DIR/edge-api/bin/" 2>/dev/null || true
@@ -211,6 +208,7 @@ replace_files() {
# 替换 deploy节点安装包 # 替换 deploy节点安装包
if [ -d "$TEMP_DIR/edge-admin/edge-api/deploy" ]; then if [ -d "$TEMP_DIR/edge-admin/edge-api/deploy" ]; then
rm -rf "$TARGET_DIR/edge-api/deploy"
mkdir -p "$TARGET_DIR/edge-api/deploy" mkdir -p "$TARGET_DIR/edge-api/deploy"
cp -r "$TEMP_DIR/edge-admin/edge-api/deploy"/* \ cp -r "$TEMP_DIR/edge-admin/edge-api/deploy"/* \
"$TARGET_DIR/edge-api/deploy/" 2>/dev/null || true "$TARGET_DIR/edge-api/deploy/" 2>/dev/null || true
@@ -219,6 +217,7 @@ replace_files() {
# 替换 installers安装工具 # 替换 installers安装工具
if [ -d "$TEMP_DIR/edge-admin/edge-api/installers" ]; then if [ -d "$TEMP_DIR/edge-admin/edge-api/installers" ]; then
rm -rf "$TARGET_DIR/edge-api/installers"
mkdir -p "$TARGET_DIR/edge-api/installers" mkdir -p "$TARGET_DIR/edge-api/installers"
cp -r "$TEMP_DIR/edge-admin/edge-api/installers"/* \ cp -r "$TEMP_DIR/edge-admin/edge-api/installers"/* \
"$TARGET_DIR/edge-api/installers/" 2>/dev/null || true "$TARGET_DIR/edge-api/installers/" 2>/dev/null || true
@@ -345,4 +344,3 @@ main() {
# 执行主函数 # 执行主函数
main main