Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
package apps
import (
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"github.com/iwind/gosock/pkg/gosock"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
// AppCmd App命令帮助
type AppCmd struct {
product string
version string
usages []string
options []*CommandHelpOption
appendStrings []string
directives []*Directive
sock *gosock.Sock
}
func NewAppCmd() *AppCmd {
return &AppCmd{
sock: gosock.NewTmpSock(teaconst.ProcessName),
}
}
type CommandHelpOption struct {
Code string
Description string
}
// Product 产品
func (this *AppCmd) Product(product string) *AppCmd {
this.product = product
return this
}
// Version 版本
func (this *AppCmd) Version(version string) *AppCmd {
this.version = version
return this
}
// Usage 使用方法
func (this *AppCmd) Usage(usage string) *AppCmd {
this.usages = append(this.usages, usage)
return this
}
// Option 选项
func (this *AppCmd) Option(code string, description string) *AppCmd {
this.options = append(this.options, &CommandHelpOption{
Code: code,
Description: description,
})
return this
}
// Append 附加内容
func (this *AppCmd) Append(appendString string) *AppCmd {
this.appendStrings = append(this.appendStrings, appendString)
return this
}
// Print 打印
func (this *AppCmd) Print() {
fmt.Println(this.product + " v" + this.version)
fmt.Println("Usage:")
for _, usage := range this.usages {
fmt.Println(" " + usage)
}
if len(this.options) > 0 {
fmt.Println("")
fmt.Println("Options:")
spaces := 20
max := 40
for _, option := range this.options {
l := len(option.Code)
if l < max && l > spaces {
spaces = l + 4
}
}
for _, option := range this.options {
if len(option.Code) > max {
fmt.Println("")
fmt.Println(" " + option.Code)
option.Code = ""
}
fmt.Printf(" %-"+strconv.Itoa(spaces)+"s%s\n", option.Code, ": "+option.Description)
}
}
if len(this.appendStrings) > 0 {
fmt.Println("")
for _, s := range this.appendStrings {
fmt.Println(s)
}
}
}
// On 添加指令
func (this *AppCmd) On(arg string, callback func()) {
this.directives = append(this.directives, &Directive{
Arg: arg,
Callback: callback,
})
}
// Run 运行
func (this *AppCmd) Run(main func()) {
// 获取参数
var args = os.Args[1:]
if len(args) > 0 {
switch args[0] {
case "-v", "version", "-version", "--version":
this.runVersion()
return
case "?", "help", "-help", "h", "-h":
this.runHelp()
return
case "start":
this.runStart()
return
case "stop":
this.runStop()
return
case "restart":
this.RunRestart()
return
case "status":
this.runStatus()
return
}
// 查找指令
for _, directive := range this.directives {
if directive.Arg == args[0] {
directive.Callback()
return
}
}
fmt.Println("unknown command '" + args[0] + "'")
return
}
// 日志
var writer = new(LogWriter)
writer.Init()
logs.SetWriter(writer)
// 运行主函数
main()
}
// 版本号
func (this *AppCmd) runVersion() {
fmt.Println(this.product+" v"+this.version, "(build: "+runtime.Version(), runtime.GOOS, runtime.GOARCH, teaconst.Tag+")")
}
// 帮助
func (this *AppCmd) runHelp() {
this.Print()
}
// 启动
func (this *AppCmd) runStart() {
var pid = this.getPID()
if pid > 0 {
fmt.Println(this.product+" already started, pid:", pid)
return
}
var cmd = exec.Command(this.exe())
err := cmd.Start()
if err != nil {
fmt.Println(this.product+" start failed:", err.Error())
return
}
// create symbolic links
_ = this.createSymLinks()
fmt.Println(this.product+" started ok, pid:", cmd.Process.Pid)
}
// 停止
func (this *AppCmd) runStop() {
var pid = this.getPID()
if pid == 0 {
fmt.Println(this.product + " not started yet")
return
}
_, _ = this.sock.Send(&gosock.Command{Code: "stop"})
fmt.Println(this.product+" stopped ok, pid:", types.String(pid))
}
// RunRestart 重启
func (this *AppCmd) RunRestart() {
this.runStop()
time.Sleep(1 * time.Second)
this.runStart()
}
// 状态
func (this *AppCmd) runStatus() {
var pid = this.getPID()
if pid == 0 {
fmt.Println(this.product + " not started yet")
return
}
fmt.Println(this.product + " is running, pid: " + types.String(pid))
}
// 获取当前的PID
func (this *AppCmd) getPID() int {
if !this.sock.IsListening() {
return 0
}
reply, err := this.sock.Send(&gosock.Command{Code: "pid"})
if err != nil {
return 0
}
return maps.NewMap(reply.Params).GetInt("pid")
}
func (this *AppCmd) exe() string {
var exe, _ = os.Executable()
if len(exe) == 0 {
exe = os.Args[0]
}
return exe
}
// 创建软链接
func (this *AppCmd) createSymLinks() error {
if runtime.GOOS != "linux" {
return nil
}
var exe, _ = os.Executable()
if len(exe) == 0 {
return nil
}
var errorList = []string{}
// bin
{
var target = "/usr/bin/" + teaconst.ProcessName
old, _ := filepath.EvalSymlinks(target)
if old != exe {
_ = os.Remove(target)
err := os.Symlink(exe, target)
if err != nil {
errorList = append(errorList, err.Error())
}
}
}
// log
{
var realPath = filepath.Dir(filepath.Dir(exe)) + "/logs/run.log"
var target = "/var/log/" + teaconst.ProcessName + ".log"
old, _ := filepath.EvalSymlinks(target)
if old != realPath {
_ = os.Remove(target)
err := os.Symlink(realPath, target)
if err != nil {
errorList = append(errorList, err.Error())
}
}
}
if len(errorList) > 0 {
return errors.New(strings.Join(errorList, "\n"))
}
return nil
}

View File

@@ -0,0 +1,6 @@
package apps
type Directive struct {
Arg string
Callback func()
}

View File

@@ -0,0 +1,108 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/sizes"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
timeutil "github.com/iwind/TeaGo/utils/time"
"log"
"os"
"runtime"
"strconv"
"strings"
)
type LogWriter struct {
fp *os.File
c chan string
}
func (this *LogWriter) Init() {
// 创建目录
var dir = files.NewFile(Tea.LogDir())
if !dir.Exists() {
err := dir.Mkdir()
if err != nil {
log.Println("[LOG]create log dir failed: " + err.Error())
}
}
// 打开要写入的日志文件
var logPath = Tea.LogFile("run.log")
fp, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Println("[LOG]open log file failed: " + err.Error())
} else {
this.fp = fp
}
this.c = make(chan string, 1024)
// 异步写入文件
var maxFileSize = 2 * sizes.G // 文件最大尺寸,超出此尺寸则清空
if fp != nil {
goman.New(func() {
var totalSize int64 = 0
stat, err := fp.Stat()
if err == nil {
totalSize = stat.Size()
}
for message := range this.c {
totalSize += int64(len(message))
_, err := fp.WriteString(timeutil.Format("Y/m/d H:i:s ") + message + "\n")
if err != nil {
log.Println("[LOG]write log failed: " + err.Error())
} else {
// 如果太大则Truncate
if totalSize > maxFileSize {
_ = fp.Truncate(0)
totalSize = 0
}
}
}
})
}
}
func (this *LogWriter) Write(message string) {
backgroundEnv, _ := os.LookupEnv("EdgeBackground")
if backgroundEnv != "on" {
// 文件和行号
var file string
var line int
if Tea.IsTesting() {
var callDepth = 3
var ok bool
_, file, line, ok = runtime.Caller(callDepth)
if ok {
file = this.packagePath(file)
}
}
if len(file) > 0 {
log.Println(message + " (" + file + ":" + strconv.Itoa(line) + ")")
} else {
log.Println(message)
}
}
this.c <- message
}
func (this *LogWriter) Close() {
if this.fp != nil {
_ = this.fp.Close()
}
close(this.c)
}
func (this *LogWriter) packagePath(path string) string {
var pieces = strings.Split(path, "/")
if len(pieces) >= 2 {
return strings.Join(pieces[len(pieces)-2:], "/")
}
return path
}

View File

@@ -0,0 +1,283 @@
package configloaders
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type AdminModuleCode = string
const (
AdminModuleCodeDashboard AdminModuleCode = "dashboard" // 看板
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
AdminModuleCodePlan AdminModuleCode = "plan" // 套餐
AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeTicket AdminModuleCode = "ticket" // 工单
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
)
var sharedAdminModuleMapping = map[int64]*AdminModuleList{} // adminId => AdminModuleList
func loadAdminModuleMapping() (map[int64]*AdminModuleList, error) {
if len(sharedAdminModuleMapping) > 0 {
return sharedAdminModuleMapping, nil
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return nil, err
}
modulesResp, err := rpcClient.AdminRPC().FindAllAdminModules(rpcClient.Context(0), &pb.FindAllAdminModulesRequest{})
if err != nil {
return nil, err
}
mapping := map[int64]*AdminModuleList{}
for _, m := range modulesResp.AdminModules {
list := &AdminModuleList{
IsSuper: m.IsSuper,
Fullname: m.Fullname,
Theme: m.Theme,
Lang: m.Lang,
}
for _, pbModule := range m.Modules {
list.Modules = append(list.Modules, &systemconfigs.AdminModule{
Code: pbModule.Code,
AllowAll: pbModule.AllowAll,
Actions: pbModule.Actions,
})
}
mapping[m.AdminId] = list
}
sharedAdminModuleMapping = mapping
return sharedAdminModuleMapping, nil
}
func NotifyAdminModuleMappingChange() error {
locker.Lock()
defer locker.Unlock()
sharedAdminModuleMapping = map[int64]*AdminModuleList{}
_, err := loadAdminModuleMapping()
return err
}
// CheckAdmin 检查用户是否存在
func CheckAdmin(adminId int64) bool {
locker.Lock()
defer locker.Unlock()
// 如果还没有数据,则尝试加载
if len(sharedAdminModuleMapping) == 0 {
_, _ = loadAdminModuleMapping()
}
_, ok := sharedAdminModuleMapping[adminId]
return ok
}
// AllowModule 检查模块是否允许访问
func AllowModule(adminId int64, module string) bool {
locker.Lock()
defer locker.Unlock()
if module == AdminModuleCodeCommon {
return true
}
if len(sharedAdminModuleMapping) == 0 {
_, _ = loadAdminModuleMapping()
}
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Allow(module)
}
return false
}
// FindFirstAdminModule 获取管理员第一个可访问模块
func FindFirstAdminModule(adminId int64) (module AdminModuleCode, ok bool) {
locker.Lock()
defer locker.Unlock()
list, ok2 := sharedAdminModuleMapping[adminId]
if ok2 {
if list.IsSuper {
return AdminModuleCodeDashboard, true
} else if len(list.Modules) > 0 {
return list.Modules[0].Code, true
}
}
return
}
// FindAdminFullname 查找某个管理员名称
func FindAdminFullname(adminId int64) string {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Fullname
}
return ""
}
// FindAdminTheme 查找某个管理员选择的风格
func FindAdminTheme(adminId int64) string {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Theme
}
return ""
}
// UpdateAdminTheme 设置某个管理员的风格
func UpdateAdminTheme(adminId int64, theme string) {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
list.Theme = theme
}
}
// FindAdminLang 查找某个管理员选择的语言
func FindAdminLang(adminId int64) string {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Lang
}
return ""
}
// UpdateAdminLang 修改某个管理员选择的语言
func UpdateAdminLang(adminId int64, langCode string) {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
list.Lang = langCode
}
}
func FindAdminLangForAction(actionPtr actions.ActionWrapper) (langCode langs.LangCode) {
locker.Lock()
defer locker.Unlock()
var adminId = actionPtr.Object().Session().GetInt64(teaconst.SessionAdminId)
list, ok := sharedAdminModuleMapping[adminId]
var result = ""
if ok {
result = list.Lang
}
if len(result) == 0 {
result = langs.ParseLangFromAction(actionPtr)
}
return result
}
// AllModuleMaps 所有权限列表
func AllModuleMaps(langCode string) []maps.Map {
var m = []maps.Map{
{
"name": langs.Message(langCode, codes.AdminMenu_Dashboard),
"code": AdminModuleCodeDashboard,
"url": "/dashboard",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Servers),
"code": AdminModuleCodeServer,
"url": "/servers",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Nodes),
"code": AdminModuleCodeNode,
"url": "/clusters",
},
{
"name": langs.Message(langCode, codes.AdminMenu_DNS),
"code": AdminModuleCodeDNS,
"url": "/dns",
},
}
if teaconst.IsPlus {
m = append(m, maps.Map{
"name": langs.Message(langCode, codes.AdminMenu_NS),
"code": AdminModuleCodeNS,
"url": "/ns",
})
}
m = append(m, []maps.Map{
{
"name": langs.Message(langCode, codes.AdminMenu_Users),
"code": AdminModuleCodeUser,
"url": "/users",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Admins),
"code": AdminModuleCodeAdmin,
"url": "/admins",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Finance),
"code": AdminModuleCodeFinance,
"url": "/finance",
},
}...)
if teaconst.IsPlus {
m = append(m, []maps.Map{
{
"name": langs.Message(langCode, codes.AdminMenu_Plans),
"code": AdminModuleCodePlan,
"url": "/plans",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Tickets),
"code": AdminModuleCodeTicket,
"url": "/tickets",
},
}...)
}
m = append(m, []maps.Map{
{
"name": langs.Message(langCode, codes.AdminMenu_Logs),
"code": AdminModuleCodeLog,
"url": "/log",
},
{
"name": langs.Message(langCode, codes.AdminMenu_Settings),
"code": AdminModuleCodeSetting,
"url": "/settings",
},
}...)
return m
}

View File

@@ -0,0 +1,3 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package configloaders

View File

@@ -0,0 +1,23 @@
package configloaders
import "github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
type AdminModuleList struct {
IsSuper bool
Modules []*systemconfigs.AdminModule
Fullname string
Theme string
Lang string
}
func (this *AdminModuleList) Allow(module string) bool {
if this.IsSuper {
return true
}
for _, m := range this.Modules {
if m.Code == module {
return true
}
}
return false
}

View File

@@ -0,0 +1,14 @@
package configloaders
import (
"github.com/iwind/TeaGo/logs"
"testing"
)
func TestLoadAdminModuleMapping(t *testing.T) {
m, err := loadAdminModuleMapping()
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(m, t)
}

View File

@@ -0,0 +1,133 @@
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"reflect"
"time"
)
var sharedAdminUIConfig *systemconfigs.AdminUIConfig = nil
func LoadAdminUIConfig() (*systemconfigs.AdminUIConfig, error) {
locker.Lock()
defer locker.Unlock()
config, err := loadAdminUIConfig()
if err != nil {
return nil, err
}
v := reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.AdminUIConfig)
return &v, nil
}
func ReloadAdminUIConfig() error {
locker.Lock()
defer locker.Unlock()
sharedAdminUIConfig = nil
_, err := loadAdminUIConfig()
return err
}
func UpdateAdminUIConfig(uiConfig *systemconfigs.AdminUIConfig) error {
locker.Lock()
defer locker.Unlock()
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return err
}
valueJSON, err := json.Marshal(uiConfig)
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: systemconfigs.SettingCodeAdminUIConfig,
ValueJSON: valueJSON,
})
if err != nil {
return err
}
sharedAdminUIConfig = uiConfig
// timezone
updateTimeZone(uiConfig)
return nil
}
// ShowFinance 是否显示财务信息
func ShowFinance() bool {
config, _ := LoadAdminUIConfig()
if config != nil && !config.ShowFinance {
return false
}
return true
}
func loadAdminUIConfig() (*systemconfigs.AdminUIConfig, error) {
if sharedAdminUIConfig != nil {
return sharedAdminUIConfig, nil
}
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: systemconfigs.SettingCodeAdminUIConfig,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedAdminUIConfig = defaultAdminUIConfig()
return sharedAdminUIConfig, nil
}
var config = &systemconfigs.AdminUIConfig{}
config.DNSResolver.Type = nodeconfigs.DNSResolverTypeDefault // 默认值
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
logs.Println("[UI_MANAGER]" + err.Error())
sharedAdminUIConfig = defaultAdminUIConfig()
return sharedAdminUIConfig, nil
}
// timezone
updateTimeZone(config)
sharedAdminUIConfig = config
return sharedAdminUIConfig, nil
}
func defaultAdminUIConfig() *systemconfigs.AdminUIConfig {
var config = &systemconfigs.AdminUIConfig{
ProductName: langs.DefaultMessage(codes.AdminUI_DefaultProductName),
AdminSystemName: langs.DefaultMessage(codes.AdminUI_DefaultSystemName),
ShowOpenSourceInfo: true,
ShowVersion: true,
ShowFinance: true,
DefaultPageSize: 10,
TimeZone: nodeconfigs.DefaultTimeZoneLocation,
}
config.DNSResolver.Type = nodeconfigs.DNSResolverTypeDefault
return config
}
// 修改时区
func updateTimeZone(config *systemconfigs.AdminUIConfig) {
if len(config.TimeZone) > 0 {
location, err := time.LoadLocation(config.TimeZone)
if err == nil && time.Local != location {
time.Local = location
}
}
}

View File

@@ -0,0 +1,29 @@
package configloaders
import (
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
"time"
)
func TestLoadUIConfig(t *testing.T) {
for i := 0; i < 10; i++ {
before := time.Now()
config, err := LoadAdminUIConfig()
if err != nil {
t.Fatal(err)
}
t.Log(time.Since(before).Seconds()*1000, "ms")
t.Logf("%p", config)
}
}
func TestLoadUIConfig2(t *testing.T) {
for i := 0; i < 10; i++ {
config, err := LoadAdminUIConfig()
if err != nil {
t.Fatal(err)
}
t.Log(config)
}
}

View File

@@ -0,0 +1,5 @@
package configloaders
import "sync"
var locker sync.Mutex

View File

@@ -0,0 +1,83 @@
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"reflect"
)
var sharedLogConfig *systemconfigs.LogConfig = nil
const (
LogSettingName = "adminLogConfig"
)
func LoadLogConfig() (*systemconfigs.LogConfig, error) {
locker.Lock()
defer locker.Unlock()
config, err := loadLogConfig()
if err != nil {
return nil, err
}
v := reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.LogConfig)
return &v, nil
}
func UpdateLogConfig(logConfig *systemconfigs.LogConfig) error {
locker.Lock()
defer locker.Unlock()
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return err
}
valueJSON, err := json.Marshal(logConfig)
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: LogSettingName,
ValueJSON: valueJSON,
})
if err != nil {
return err
}
sharedLogConfig = logConfig
return nil
}
func loadLogConfig() (*systemconfigs.LogConfig, error) {
if sharedLogConfig != nil {
return sharedLogConfig, nil
}
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: LogSettingName,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedLogConfig = systemconfigs.DefaultLogConfig()
return sharedLogConfig, nil
}
config := &systemconfigs.LogConfig{}
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
logs.Println("[LOG_MANAGER]" + err.Error())
sharedLogConfig = systemconfigs.DefaultLogConfig()
return sharedLogConfig, nil
}
sharedLogConfig = config
return sharedLogConfig, nil
}

View File

@@ -0,0 +1,118 @@
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"reflect"
)
const (
SecuritySettingName = "adminSecurityConfig"
FrameNone = ""
FrameDeny = "DENY"
FrameSameOrigin = "SAMEORIGIN"
)
var sharedSecurityConfig *systemconfigs.SecurityConfig = nil
func LoadSecurityConfig() (*systemconfigs.SecurityConfig, error) {
locker.Lock()
defer locker.Unlock()
config, err := loadSecurityConfig()
if err != nil {
return nil, err
}
var v = reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.SecurityConfig)
return &v, nil
}
func UpdateSecurityConfig(securityConfig *systemconfigs.SecurityConfig) error {
locker.Lock()
defer locker.Unlock()
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return err
}
valueJSON, err := json.Marshal(securityConfig)
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: SecuritySettingName,
ValueJSON: valueJSON,
})
if err != nil {
return err
}
err = securityConfig.Init()
if err != nil {
return err
}
sharedSecurityConfig = securityConfig
// 通知更新
events.Notify(events.EventSecurityConfigChanged)
return nil
}
func loadSecurityConfig() (*systemconfigs.SecurityConfig, error) {
if sharedSecurityConfig != nil {
return sharedSecurityConfig, nil
}
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: SecuritySettingName,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedSecurityConfig = NewSecurityConfig()
return sharedSecurityConfig, nil
}
var config = &systemconfigs.SecurityConfig{
Frame: FrameSameOrigin,
AllowLocal: true,
CheckClientFingerprint: false,
CheckClientRegion: true,
DenySearchEngines: true,
DenySpiders: true,
}
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
logs.Println("[SECURITY_MANAGER]" + err.Error())
sharedSecurityConfig = NewSecurityConfig()
return sharedSecurityConfig, nil
}
err = config.Init()
if err != nil {
return nil, err
}
sharedSecurityConfig = config
return sharedSecurityConfig, nil
}
// NewSecurityConfig create new security config
func NewSecurityConfig() *systemconfigs.SecurityConfig {
return &systemconfigs.SecurityConfig{
Frame: FrameSameOrigin,
AllowLocal: true,
CheckClientFingerprint: false,
CheckClientRegion: true,
DenySearchEngines: true,
DenySpiders: true,
}
}

View File

@@ -0,0 +1,32 @@
package configloaders
import (
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
"time"
)
func TestLoadSecurityConfig(t *testing.T) {
for i := 0; i < 10; i++ {
before := time.Now()
config, err := LoadSecurityConfig()
if err != nil {
t.Fatal(err)
}
t.Log(time.Since(before).Seconds()*1000, "ms")
t.Logf("%p", config)
}
}
func TestLoadSecurityConfig2(t *testing.T) {
for i := 0; i < 10; i++ {
config, err := LoadSecurityConfig()
if err != nil {
t.Fatal(err)
}
if i == 0 {
config.Frame = "DENY"
}
t.Log(config.Frame)
}
}

View File

@@ -0,0 +1,79 @@
//go:build plus
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/logs"
"reflect"
)
var sharedUserPriceConfig *userconfigs.UserPriceConfig = nil
func LoadUserPriceConfig() (*userconfigs.UserPriceConfig, error) {
locker.Lock()
defer locker.Unlock()
config, err := loadUserPriceConfig()
if err != nil {
return nil, err
}
var v = reflect.Indirect(reflect.ValueOf(config)).Interface().(userconfigs.UserPriceConfig)
return &v, nil
}
func ResetUserPriceConfig(priceConfig *userconfigs.UserPriceConfig) {
sharedUserPriceConfig = priceConfig
}
func UpdateUserPriceConfig(priceConfig *userconfigs.UserPriceConfig) error {
priceConfigJSON, err := json.Marshal(priceConfig)
if err != nil {
return err
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: systemconfigs.SettingCodeUserPriceConfig,
ValueJSON: priceConfigJSON,
})
sharedUserPriceConfig = priceConfig
return err
}
func loadUserPriceConfig() (*userconfigs.UserPriceConfig, error) {
if sharedUserPriceConfig != nil {
return sharedUserPriceConfig, nil
}
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: systemconfigs.SettingCodeUserPriceConfig,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedUserPriceConfig = userconfigs.DefaultUserPriceConfig()
return sharedUserPriceConfig, nil
}
var config = userconfigs.DefaultUserPriceConfig()
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
logs.Println("loadUserPriceConfig: " + err.Error())
sharedUserPriceConfig = userconfigs.DefaultUserPriceConfig()
return sharedUserPriceConfig, nil
}
sharedUserPriceConfig = config
return sharedUserPriceConfig, nil
}

View File

@@ -0,0 +1,35 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
)
// LoadUserSenderConfig 加载用户媒介发送配置
func LoadUserSenderConfig() (*userconfigs.UserSenderConfig, error) {
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
configResp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserSenderConfig})
if err != nil {
return nil, err
}
var data = configResp.ValueJSON
if len(data) == 0 {
return userconfigs.DefaultUserSenderConfig(), nil
}
var config = userconfigs.DefaultUserSenderConfig()
err = json.Unmarshal(data, config)
if err != nil {
return nil, err
}
return config, nil
}

View File

@@ -0,0 +1,81 @@
//go:build plus
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"reflect"
)
var sharedUserUIConfig *systemconfigs.UserUIConfig = nil
func LoadUserUIConfig() (*systemconfigs.UserUIConfig, error) {
locker.Lock()
defer locker.Unlock()
config, err := loadUserUIConfig()
if err != nil {
return nil, err
}
v := reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.UserUIConfig)
return &v, nil
}
func UpdateUserUIConfig(uiConfig *systemconfigs.UserUIConfig) error {
locker.Lock()
defer locker.Unlock()
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return err
}
valueJSON, err := json.Marshal(uiConfig)
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: systemconfigs.SettingCodeUserUIConfig,
ValueJSON: valueJSON,
})
if err != nil {
return err
}
sharedUserUIConfig = uiConfig
return nil
}
func loadUserUIConfig() (*systemconfigs.UserUIConfig, error) {
if sharedUserUIConfig != nil {
return sharedUserUIConfig, nil
}
var rpcClient, err = rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: systemconfigs.SettingCodeUserUIConfig,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedUserUIConfig = systemconfigs.NewUserUIConfig()
return sharedUserUIConfig, nil
}
var config = systemconfigs.NewUserUIConfig()
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
logs.Println("[UI_MANAGER]" + err.Error())
sharedUserUIConfig = systemconfigs.NewUserUIConfig()
return sharedUserUIConfig, nil
}
sharedUserUIConfig = config
return sharedUserUIConfig, nil
}

View File

@@ -0,0 +1,226 @@
package configs
import (
"errors"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/Tea"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
)
const ConfigFileName = "api_admin.yaml"
const oldConfigFileName = "api.yaml"
// APIConfig API配置
type APIConfig struct {
OldRPC struct {
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
} `yaml:"rpc,omitempty"`
RPCEndpoints []string `yaml:"rpc.endpoints,flow" json:"rpc.endpoints"`
RPCDisableUpdate bool `yaml:"rpc.disableUpdate" json:"rpc.disableUpdate"`
NodeId string `yaml:"nodeId"`
Secret string `yaml:"secret"`
}
// LoadAPIConfig 加载API配置
func LoadAPIConfig() (*APIConfig, error) {
// 候选文件
var realFile = Tea.ConfigFile(ConfigFileName)
var oldRealFile = Tea.ConfigFile(oldConfigFileName)
var isFromLocal = false
var paths = []string{realFile, oldRealFile}
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/"+ConfigFileName)
}
paths = append(paths, "/etc/"+teaconst.ProcessName+"/"+ConfigFileName)
var data []byte
var err error
var isFromOld = false
for _, path := range paths {
data, err = os.ReadFile(path)
if err == nil {
if path == realFile || path == oldRealFile {
isFromLocal = true
}
// 自动生成新的配置文件
isFromOld = path == oldRealFile
break
}
}
if err != nil {
return nil, err
}
var config = &APIConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return nil, err
}
err = config.Init()
if err != nil {
return nil, errors.New("init error: " + err.Error())
}
if !isFromLocal {
// 恢复文件
_ = os.WriteFile(realFile, data, 0666)
}
// 自动生成新配置文件
if isFromOld {
config.OldRPC.Endpoints = nil
_ = config.WriteFile(Tea.ConfigFile(ConfigFileName))
}
return config, nil
}
// ResetAPIConfig 重置配置
func ResetAPIConfig() error {
var filename = ConfigFileName
// 重置 configs/api_admin.yaml
{
var configFile = Tea.ConfigFile(filename)
stat, err := os.Stat(configFile)
if err == nil && !stat.IsDir() {
err = os.Remove(configFile)
if err != nil {
return err
}
}
}
// 重置 ~/.edge-admin/api_admin.yaml
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
var configFile = homeDir + "/." + teaconst.ProcessName + "/" + filename
stat, err := os.Stat(configFile)
if err == nil && !stat.IsDir() {
err = os.Remove(configFile)
if err != nil {
return err
}
}
}
// 重置 /etc/edge-admin/api_admin.yaml
{
var configFile = "/etc/" + teaconst.ProcessName + "/" + filename
stat, err := os.Stat(configFile)
if err == nil && !stat.IsDir() {
err = os.Remove(configFile)
if err != nil {
return err
}
}
}
return nil
}
func IsNewInstalled() bool {
homeDir, err := os.UserHomeDir()
if err != nil {
return false
}
for _, filename := range []string{ConfigFileName, oldConfigFileName} {
_, err = os.Stat(homeDir + "/." + teaconst.ProcessName + "/" + filename)
if err == nil {
return false
}
}
return true
}
// WriteFile 写入API配置
func (this *APIConfig) WriteFile(path string) error {
data, err := yaml.Marshal(this)
if err != nil {
return err
}
err = os.WriteFile(path, data, 0666)
if err != nil {
return err
}
// 写入 ~/ 和 /etc/ 目录,因为是备份需要,所以不需要提示错误信息
// 写入 ~/.edge-admin/
// 这个用来判断用户是否为重装,所以比较重要
var filename = filepath.Base(path)
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
dir := homeDir + "/." + teaconst.ProcessName
stat, err := os.Stat(dir)
if err == nil && stat.IsDir() {
err = os.WriteFile(dir+"/"+filename, data, 0666)
if err != nil {
return err
}
} else if err != nil && os.IsNotExist(err) {
err = os.Mkdir(dir, 0777)
if err == nil {
err = os.WriteFile(dir+"/"+filename, data, 0666)
if err != nil {
return err
}
}
}
}
// 写入 /etc/edge-admin
{
var dir = "/etc/" + teaconst.ProcessName
stat, err := os.Stat(dir)
if err == nil && stat.IsDir() {
_ = os.WriteFile(dir+"/"+filename, data, 0666)
} else if err != nil && os.IsNotExist(err) {
err = os.Mkdir(dir, 0777)
if err == nil {
_ = os.WriteFile(dir+"/"+filename, data, 0666)
}
}
}
return nil
}
// Clone 克隆当前配置
func (this *APIConfig) Clone() *APIConfig {
return &APIConfig{
NodeId: this.NodeId,
Secret: this.Secret,
}
}
func (this *APIConfig) Init() error {
// compatible with old
if len(this.RPCEndpoints) == 0 && len(this.OldRPC.Endpoints) > 0 {
this.RPCEndpoints = this.OldRPC.Endpoints
this.RPCDisableUpdate = this.OldRPC.DisableUpdate
}
if len(this.RPCEndpoints) == 0 {
return errors.New("no valid 'rpc.endpoints'")
}
if len(this.NodeId) == 0 {
return errors.New("'nodeId' required")
}
if len(this.Secret) == 0 {
return errors.New("'secret' required")
}
return nil
}

View File

@@ -0,0 +1,30 @@
package configs
import (
_ "github.com/iwind/TeaGo/bootstrap"
"gopkg.in/yaml.v3"
"testing"
)
func TestLoadAPIConfig(t *testing.T) {
config, err := LoadAPIConfig()
if err != nil {
t.Fatal(err)
}
t.Log(config)
configData, err := yaml.Marshal(config)
if err != nil {
t.Fatal(err)
}
t.Log(string(configData))
}
func TestAPIConfig_WriteFile(t *testing.T) {
var config = &APIConfig{}
err := config.WriteFile("/tmp/api_config.yaml")
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,6 @@
package nodes
type NodeConfig struct {
Id string `json:"id" yaml:"id"`
Secret string `json:"secret" yaml:"secret"`
}

View File

@@ -0,0 +1,44 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package configs
import (
"encoding/json"
"github.com/iwind/TeaGo/Tea"
"os"
)
var plusConfigFile = "plus.cache.json"
type PlusConfig struct {
IsPlus bool `json:"isPlus"`
Edition string `json:"edition"`
Components []string `json:"components"`
DayTo string `json:"dayTo"`
}
func ReadPlusConfig() *PlusConfig {
data, err := os.ReadFile(Tea.ConfigFile(plusConfigFile))
if err != nil {
return &PlusConfig{IsPlus: false}
}
var config = &PlusConfig{IsPlus: false}
err = json.Unmarshal(data, config)
if err != nil {
return config
}
return config
}
func WritePlusConfig(config *PlusConfig) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
err = os.WriteFile(Tea.ConfigFile(plusConfigFile), configJSON, 0777)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,3 @@
package configs
var Secret = ""

View File

@@ -0,0 +1,51 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package configs
import (
"fmt"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"gopkg.in/yaml.v3"
"net/url"
"os"
)
type SimpleDBConfig struct {
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
Host string `yaml:"host"`
BoolFields []string `yaml:"boolFields,omitempty"`
}
func (this *SimpleDBConfig) GenerateOldConfig(targetFile string) error {
var dbConfig = &dbs.DBConfig{
Driver: "mysql",
Dsn: url.QueryEscape(this.User) + ":" + url.QueryEscape(this.Password) + "@tcp(" + this.Host + ")/" + url.PathEscape(this.Database) + "?charset=utf8mb4&timeout=30s&multiStatements=true",
Prefix: "edge",
}
dbConfig.Models.Package = "internal/db/models"
var config = &dbs.Config{
DBs: map[string]*dbs.DBConfig{
Tea.Env: dbConfig,
},
}
config.Default.DB = Tea.Env
config.Fields = map[string][]string{
"bool": this.BoolFields,
}
oldConfigYAML, encodeErr := yaml.Marshal(config)
if encodeErr != nil {
return encodeErr
}
err := os.WriteFile(targetFile, oldConfigYAML, 0666)
if err != nil {
return fmt.Errorf("create database config file failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,8 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build !plus
package teaconst
const BuildCommunity = true
const BuildPlus = false
const Tag = "community"

View File

@@ -0,0 +1,8 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package teaconst
const BuildCommunity = false
const BuildPlus = true
const Tag = "plus"

View File

@@ -0,0 +1,21 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
APINodeVersion = "1.4.7" //1.3.9
ProductName = "Edge Admin"
ProcessName = "edge-admin"
Role = "admin"
EncryptMethod = "aes-256-cfb"
CookieSID = "geadsid"
SessionAdminId = "adminId"
SystemdServiceName = "edge-admin"
UpdatesURL = "https://goedge.cn/api/boot/versions?os=${os}&arch=${arch}&version=${version}"
)

View File

@@ -0,0 +1,6 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build !plus
package teaconst
var IsPlus = false

View File

@@ -0,0 +1,6 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package teaconst
var IsPlus = false

View File

@@ -0,0 +1,32 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package teaconst
import (
"os"
"strings"
)
var (
IsRecoverMode = false
IsDemoMode = false
ErrorDemoOperation = "DEMO模式下无法进行创建、修改、删除等操作"
NewVersionCode = "" // 有新的版本
NewVersionDownloadURL = "" // 新版本下载地址
IsMain = checkMain()
)
// 检查是否为主程序
func checkMain() bool {
if len(os.Args) == 1 ||
(len(os.Args) >= 2 && os.Args[1] == "pprof") {
return true
}
exe, _ := os.Executable()
return strings.HasSuffix(exe, ".test") ||
strings.HasSuffix(exe, ".test.exe") ||
strings.Contains(exe, "___")
}

View File

@@ -0,0 +1,58 @@
package csrf
import (
"sync"
"time"
)
var sharedTokenManager = NewTokenManager()
func init() {
go func() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
sharedTokenManager.Clean()
}
}()
}
type TokenManager struct {
tokenMap map[string]int64 // token => timestamp
locker sync.Mutex
}
func NewTokenManager() *TokenManager {
return &TokenManager{
tokenMap: map[string]int64{},
}
}
func (this *TokenManager) Put(token string) {
this.locker.Lock()
this.tokenMap[token] = time.Now().Unix()
this.locker.Unlock()
}
func (this *TokenManager) Exists(token string) bool {
this.locker.Lock()
_, ok := this.tokenMap[token]
this.locker.Unlock()
return ok
}
func (this *TokenManager) Delete(token string) {
this.locker.Lock()
delete(this.tokenMap, token)
this.locker.Unlock()
}
func (this *TokenManager) Clean() {
this.locker.Lock()
for token, timestamp := range this.tokenMap {
if time.Now().Unix()-timestamp > 3600 { // 删除一个小时前的
delete(this.tokenMap, token)
}
}
this.locker.Unlock()
}

View File

@@ -0,0 +1,66 @@
package csrf
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/iwind/TeaGo/types"
"strconv"
"time"
)
// Generate 生成Token
func Generate() string {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
h := sha256.New()
h.Write([]byte(configs.Secret))
h.Write([]byte(timestamp))
s := h.Sum(nil)
token := base64.StdEncoding.EncodeToString([]byte(timestamp + fmt.Sprintf("%x", s)))
sharedTokenManager.Put(token)
return token
}
// Validate 校验Token
func Validate(token string) (b bool) {
if len(token) == 0 {
return
}
if !sharedTokenManager.Exists(token) {
return
}
defer func() {
sharedTokenManager.Delete(token)
}()
data, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return
}
hashString := string(data)
if len(hashString) < 10+32 {
return
}
timestampString := hashString[:10]
hashString = hashString[10:]
h := sha256.New()
h.Write([]byte(configs.Secret))
h.Write([]byte(timestampString))
hashData := h.Sum(nil)
if hashString != fmt.Sprintf("%x", hashData) {
return
}
timestamp := types.Int64(timestampString)
if timestamp < time.Now().Unix()-1800 { // 有效期半个小时
return
}
return true
}

View File

@@ -0,0 +1,41 @@
package encrypt
import (
"github.com/iwind/TeaGo/logs"
)
const (
MagicKey = "f1c8eafb543f03023e97b7be864a4e9b"
)
// 加密特殊信息
func MagicKeyEncode(data []byte) []byte {
method, err := NewMethodInstance("aes-256-cfb", MagicKey, MagicKey[:16])
if err != nil {
logs.Println("[MagicKeyEncode]" + err.Error())
return data
}
dst, err := method.Encrypt(data)
if err != nil {
logs.Println("[MagicKeyEncode]" + err.Error())
return data
}
return dst
}
// 解密特殊信息
func MagicKeyDecode(data []byte) []byte {
method, err := NewMethodInstance("aes-256-cfb", MagicKey, MagicKey[:16])
if err != nil {
logs.Println("[MagicKeyEncode]" + err.Error())
return data
}
src, err := method.Decrypt(data)
if err != nil {
logs.Println("[MagicKeyEncode]" + err.Error())
return data
}
return src
}

View File

@@ -0,0 +1,11 @@
package encrypt
import "testing"
func TestMagicKeyEncode(t *testing.T) {
dst := MagicKeyEncode([]byte("Hello,World"))
t.Log("dst:", string(dst))
src := MagicKeyDecode(dst)
t.Log("src:", string(src))
}

View File

@@ -0,0 +1,12 @@
package encrypt
type MethodInterface interface {
// 初始化
Init(key []byte, iv []byte) error
// 加密
Encrypt(src []byte) (dst []byte, err error)
// 解密
Decrypt(dst []byte) (src []byte, err error)
}

View File

@@ -0,0 +1,73 @@
package encrypt
import (
"bytes"
"crypto/aes"
"crypto/cipher"
)
type AES128CFBMethod struct {
iv []byte
block cipher.Block
}
func (this *AES128CFBMethod) Init(key, iv []byte) error {
// 判断key是否为32长度
l := len(key)
if l > 16 {
key = key[:16]
} else if l < 16 {
key = append(key, bytes.Repeat([]byte{' '}, 16-l)...)
}
// 判断iv长度
l2 := len(iv)
if l2 > aes.BlockSize {
iv = iv[:aes.BlockSize]
} else if l2 < aes.BlockSize {
iv = append(iv, bytes.Repeat([]byte{' '}, aes.BlockSize-l2)...)
}
this.iv = iv
// block
block, err := aes.NewCipher(key)
if err != nil {
return err
}
this.block = block
return nil
}
func (this *AES128CFBMethod) Encrypt(src []byte) (dst []byte, err error) {
if len(src) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
dst = make([]byte, len(src))
encrypter := cipher.NewCFBEncrypter(this.block, this.iv)
encrypter.XORKeyStream(dst, src)
return
}
func (this *AES128CFBMethod) Decrypt(dst []byte) (src []byte, err error) {
if len(dst) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
src = make([]byte, len(dst))
encrypter := cipher.NewCFBDecrypter(this.block, this.iv)
encrypter.XORKeyStream(src, dst)
return
}

View File

@@ -0,0 +1,90 @@
package encrypt
import (
"runtime"
"strings"
"testing"
)
func TestAES128CFBMethod_Encrypt(t *testing.T) {
method, err := NewMethodInstance("aes-128-cfb", "abc", "123")
if err != nil {
t.Fatal(err)
}
src := []byte("Hello, World")
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
dst = dst[:len(src)]
t.Log("dst:", string(dst))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
}
t.Log("src:", string(src))
}
func TestAES128CFBMethod_Encrypt2(t *testing.T) {
method, err := NewMethodInstance("aes-128-cfb", "abc", "123")
if err != nil {
t.Fatal(err)
}
sources := [][]byte{}
{
a := []byte{1}
_, err = method.Encrypt(a)
if err != nil {
t.Fatal(err)
}
}
for i := 0; i < 10; i++ {
src := []byte(strings.Repeat("Hello", 1))
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
sources = append(sources, dst)
}
{
a := []byte{1}
_, err = method.Decrypt(a)
if err != nil {
t.Fatal(err)
}
}
for _, dst := range sources {
dst2 := append([]byte{}, dst...)
src2, err := method.Decrypt(dst2)
if err != nil {
t.Fatal(err)
}
t.Log(string(src2))
}
}
func BenchmarkAES128CFBMethod_Encrypt(b *testing.B) {
runtime.GOMAXPROCS(1)
method, err := NewMethodInstance("aes-128-cfb", "abc", "123")
if err != nil {
b.Fatal(err)
}
src := []byte(strings.Repeat("Hello", 1024))
for i := 0; i < b.N; i++ {
dst, err := method.Encrypt(src)
if err != nil {
b.Fatal(err)
}
_ = dst
}
}

View File

@@ -0,0 +1,74 @@
package encrypt
import (
"bytes"
"crypto/aes"
"crypto/cipher"
)
type AES192CFBMethod struct {
block cipher.Block
iv []byte
}
func (this *AES192CFBMethod) Init(key, iv []byte) error {
// 判断key是否为24长度
l := len(key)
if l > 24 {
key = key[:24]
} else if l < 24 {
key = append(key, bytes.Repeat([]byte{' '}, 24-l)...)
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
this.block = block
// 判断iv长度
l2 := len(iv)
if l2 > aes.BlockSize {
iv = iv[:aes.BlockSize]
} else if l2 < aes.BlockSize {
iv = append(iv, bytes.Repeat([]byte{' '}, aes.BlockSize-l2)...)
}
this.iv = iv
return nil
}
func (this *AES192CFBMethod) Encrypt(src []byte) (dst []byte, err error) {
if len(src) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
dst = make([]byte, len(src))
encrypter := cipher.NewCFBEncrypter(this.block, this.iv)
encrypter.XORKeyStream(dst, src)
return
}
func (this *AES192CFBMethod) Decrypt(dst []byte) (src []byte, err error) {
if len(dst) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
src = make([]byte, len(dst))
decrypter := cipher.NewCFBDecrypter(this.block, this.iv)
decrypter.XORKeyStream(src, dst)
return
}

View File

@@ -0,0 +1,45 @@
package encrypt
import (
"runtime"
"strings"
"testing"
)
func TestAES192CFBMethod_Encrypt(t *testing.T) {
method, err := NewMethodInstance("aes-192-cfb", "abc", "123")
if err != nil {
t.Fatal(err)
}
src := []byte("Hello, World")
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
dst = dst[:len(src)]
t.Log("dst:", string(dst))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
}
t.Log("src:", string(src))
}
func BenchmarkAES192CFBMethod_Encrypt(b *testing.B) {
runtime.GOMAXPROCS(1)
method, err := NewMethodInstance("aes-192-cfb", "abc", "123")
if err != nil {
b.Fatal(err)
}
src := []byte(strings.Repeat("Hello", 1024))
for i := 0; i < b.N; i++ {
dst, err := method.Encrypt(src)
if err != nil {
b.Fatal(err)
}
_ = dst
}
}

View File

@@ -0,0 +1,72 @@
package encrypt
import (
"bytes"
"crypto/aes"
"crypto/cipher"
)
type AES256CFBMethod struct {
block cipher.Block
iv []byte
}
func (this *AES256CFBMethod) Init(key, iv []byte) error {
// 判断key是否为32长度
l := len(key)
if l > 32 {
key = key[:32]
} else if l < 32 {
key = append(key, bytes.Repeat([]byte{' '}, 32-l)...)
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
this.block = block
// 判断iv长度
l2 := len(iv)
if l2 > aes.BlockSize {
iv = iv[:aes.BlockSize]
} else if l2 < aes.BlockSize {
iv = append(iv, bytes.Repeat([]byte{' '}, aes.BlockSize-l2)...)
}
this.iv = iv
return nil
}
func (this *AES256CFBMethod) Encrypt(src []byte) (dst []byte, err error) {
if len(src) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
dst = make([]byte, len(src))
encrypter := cipher.NewCFBEncrypter(this.block, this.iv)
encrypter.XORKeyStream(dst, src)
return
}
func (this *AES256CFBMethod) Decrypt(dst []byte) (src []byte, err error) {
if len(dst) == 0 {
return
}
defer func() {
err = RecoverMethodPanic(recover())
}()
src = make([]byte, len(dst))
decrypter := cipher.NewCFBDecrypter(this.block, this.iv)
decrypter.XORKeyStream(src, dst)
return
}

View File

@@ -0,0 +1,42 @@
package encrypt
import "testing"
func TestAES256CFBMethod_Encrypt(t *testing.T) {
method, err := NewMethodInstance("aes-256-cfb", "abc", "123")
if err != nil {
t.Fatal(err)
}
src := []byte("Hello, World")
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
dst = dst[:len(src)]
t.Log("dst:", string(dst))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
}
t.Log("src:", string(src))
}
func TestAES256CFBMethod_Encrypt2(t *testing.T) {
method, err := NewMethodInstance("aes-256-cfb", "abc", "123")
if err != nil {
t.Fatal(err)
}
src := []byte("Hello, World")
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
t.Log("dst:", string(dst))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
}
t.Log("src:", string(src))
}

View File

@@ -0,0 +1,26 @@
package encrypt
type RawMethod struct {
}
func (this *RawMethod) Init(key, iv []byte) error {
return nil
}
func (this *RawMethod) Encrypt(src []byte) (dst []byte, err error) {
if len(src) == 0 {
return
}
dst = make([]byte, len(src))
copy(dst, src)
return
}
func (this *RawMethod) Decrypt(dst []byte) (src []byte, err error) {
if len(dst) == 0 {
return
}
src = make([]byte, len(dst))
copy(src, dst)
return
}

View File

@@ -0,0 +1,23 @@
package encrypt
import "testing"
func TestRawMethod_Encrypt(t *testing.T) {
method, err := NewMethodInstance("raw", "abc", "123")
if err != nil {
t.Fatal(err)
}
src := []byte("Hello, World")
dst, err := method.Encrypt(src)
if err != nil {
t.Fatal(err)
}
dst = dst[:len(src)]
t.Log("dst:", string(dst))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
}
t.Log("src:", string(src))
}

View File

@@ -0,0 +1,43 @@
package encrypt
import (
"errors"
"reflect"
)
var methods = map[string]reflect.Type{
"raw": reflect.TypeOf(new(RawMethod)).Elem(),
"aes-128-cfb": reflect.TypeOf(new(AES128CFBMethod)).Elem(),
"aes-192-cfb": reflect.TypeOf(new(AES192CFBMethod)).Elem(),
"aes-256-cfb": reflect.TypeOf(new(AES256CFBMethod)).Elem(),
}
func NewMethodInstance(method string, key string, iv string) (MethodInterface, error) {
valueType, ok := methods[method]
if !ok {
return nil, errors.New("method '" + method + "' not found")
}
instance, ok := reflect.New(valueType).Interface().(MethodInterface)
if !ok {
return nil, errors.New("method '" + method + "' must implement MethodInterface")
}
err := instance.Init([]byte(key), []byte(iv))
return instance, err
}
func RecoverMethodPanic(err interface{}) error {
if err != nil {
s, ok := err.(string)
if ok {
return errors.New(s)
}
e, ok := err.(error)
if ok {
return e
}
return errors.New("unknown error")
}
return nil
}

View File

@@ -0,0 +1,8 @@
package encrypt
import "testing"
func TestFindMethodInstance(t *testing.T) {
t.Log(NewMethodInstance("a", "b", ""))
t.Log(NewMethodInstance("aes-256-cfb", "123456", ""))
}

View File

@@ -0,0 +1,56 @@
package errors
import (
"errors"
"path/filepath"
"runtime"
"strconv"
)
type errorObj struct {
err error
file string
line int
funcName string
}
func (this *errorObj) Error() string {
s := this.err.Error() + "\n " + this.file
if len(this.funcName) > 0 {
s += ":" + this.funcName + "()"
}
s += ":" + strconv.Itoa(this.line)
return s
}
// 新错误
func New(errText string) error {
ptr, file, line, ok := runtime.Caller(1)
funcName := ""
if ok {
frame, _ := runtime.CallersFrames([]uintptr{ptr}).Next()
funcName = filepath.Base(frame.Function)
}
return &errorObj{
err: errors.New(errText),
file: file,
line: line,
funcName: funcName,
}
}
// 包装已有错误
func Wrap(err error) error {
ptr, file, line, ok := runtime.Caller(1)
funcName := ""
if ok {
frame, _ := runtime.CallersFrames([]uintptr{ptr}).Next()
funcName = filepath.Base(frame.Function)
}
return &errorObj{
err: err,
file: file,
line: line,
funcName: funcName,
}
}

View File

@@ -0,0 +1,22 @@
package errors
import (
"errors"
"testing"
)
func TestNew(t *testing.T) {
t.Log(New("hello"))
t.Log(Wrap(errors.New("hello")))
t.Log(testError1())
t.Log(Wrap(testError1()))
t.Log(Wrap(testError2()))
}
func testError1() error {
return New("test error1")
}
func testError2() error {
return Wrap(testError1())
}

View File

@@ -0,0 +1,10 @@
package events
type Event = string
const (
EventStart Event = "start" // start loading
EventQuit Event = "quit" // quit node gracefully
EventSecurityConfigChanged Event = "securityConfigChanged" // 安全设置变更
)

View File

@@ -0,0 +1,27 @@
package events
import "sync"
var eventsMap = map[string][]func(){} // event => []callbacks
var locker = sync.Mutex{}
// On 增加事件回调
func On(event string, callback func()) {
locker.Lock()
defer locker.Unlock()
var callbacks = eventsMap[event]
callbacks = append(callbacks, callback)
eventsMap[event] = callbacks
}
// Notify 通知事件
func Notify(event string) {
locker.Lock()
var callbacks = eventsMap[event]
locker.Unlock()
for _, callback := range callbacks {
callback()
}
}

View File

@@ -0,0 +1,16 @@
package events
import "testing"
func TestOn(t *testing.T) {
On("hello", func() {
t.Log("world")
})
On("hello", func() {
t.Log("world2")
})
On("hello2", func() {
t.Log("world2")
})
Notify("hello")
}

View File

@@ -0,0 +1,170 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package gen
import (
"bytes"
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/conds/condutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"io"
"os"
"path/filepath"
)
func Generate() error {
err := generateComponentsJSFile()
if err != nil {
return fmt.Errorf("generate 'components.src.js' failed: %w", err)
}
return nil
}
// 生成Javascript文件
func generateComponentsJSFile() error {
var buffer = bytes.NewBuffer([]byte{})
var webRoot string
if Tea.IsTesting() {
webRoot = Tea.Root + "/../web/public/js/components/"
} else {
webRoot = Tea.Root + "/web/public/js/components/"
}
var f = files.NewFile(webRoot)
f.Range(func(file *files.File) {
if !file.IsFile() {
return
}
if file.Ext() != ".js" {
return
}
data, err := file.ReadAll()
if err != nil {
logs.Error(err)
return
}
buffer.Write(data)
buffer.Write([]byte{'\n', '\n'})
})
// 条件组件
typesJSON, err := json.Marshal(condutils.ReadAllAvailableCondTypes())
if err != nil {
logs.Println("ComponentsAction marshal request cond types failed: " + err.Error())
} else {
buffer.WriteString("window.REQUEST_COND_COMPONENTS = ")
buffer.Write(typesJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// 条件操作符
requestOperatorsJSON, err := json.Marshal(shared.AllRequestOperators())
if err != nil {
logs.Println("ComponentsAction marshal request operators failed: " + err.Error())
} else {
buffer.WriteString("window.REQUEST_COND_OPERATORS = ")
buffer.Write(requestOperatorsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// 请求变量
requestVariablesJSON, err := json.Marshal(shared.DefaultRequestVariables())
if err != nil {
logs.Println("ComponentsAction marshal request variables failed: " + err.Error())
} else {
buffer.WriteString("window.REQUEST_VARIABLES = ")
buffer.Write(requestVariablesJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// 指标
metricHTTPKeysJSON, err := json.Marshal(serverconfigs.FindAllMetricKeyDefinitions(serverconfigs.MetricItemCategoryHTTP))
if err != nil {
logs.Println("ComponentsAction marshal metric http keys failed: " + err.Error())
} else {
buffer.WriteString("window.METRIC_HTTP_KEYS = ")
buffer.Write(metricHTTPKeysJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// IP地址阈值项目
ipAddrThresholdItemsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdItems())
if err != nil {
logs.Println("ComponentsAction marshal ip addr threshold items failed: " + err.Error())
} else {
buffer.WriteString("window.IP_ADDR_THRESHOLD_ITEMS = ")
buffer.Write(ipAddrThresholdItemsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// IP地址阈值动作
ipAddrThresholdActionsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdActions())
if err != nil {
logs.Println("ComponentsAction marshal ip addr threshold actions failed: " + err.Error())
} else {
buffer.WriteString("window.IP_ADDR_THRESHOLD_ACTIONS = ")
buffer.Write(ipAddrThresholdActionsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF checkpoints
var wafCheckpointsMaps = []maps.Map{}
for _, checkpoint := range firewallconfigs.AllCheckpoints {
wafCheckpointsMaps = append(wafCheckpointsMaps, maps.Map{
"name": checkpoint.Name,
"prefix": checkpoint.Prefix,
"description": checkpoint.Description,
})
}
wafCheckpointsJSON, err := json.Marshal(wafCheckpointsMaps)
if err != nil {
logs.Println("ComponentsAction marshal waf rule checkpoints failed: " + err.Error())
} else {
buffer.WriteString("window.WAF_RULE_CHECKPOINTS = ")
buffer.Write(wafCheckpointsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF操作符
wafOperatorsJSON, err := json.Marshal(firewallconfigs.AllRuleOperators)
if err != nil {
logs.Println("ComponentsAction marshal waf rule operators failed: " + err.Error())
} else {
buffer.WriteString("window.WAF_RULE_OPERATORS = ")
buffer.Write(wafOperatorsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF验证码类型
captchaTypesJSON, err := json.Marshal(firewallconfigs.FindAllCaptchaTypes())
if err != nil {
logs.Println("ComponentsAction marshal captcha types failed: " + err.Error())
} else {
buffer.WriteString("window.WAF_CAPTCHA_TYPES = ")
buffer.Write(captchaTypesJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
fp, err := os.OpenFile(filepath.Clean(Tea.PublicFile("/js/components.src.js")), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
if err != nil {
return err
}
_, err = io.Copy(fp, buffer)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,13 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package gen
import "testing"
func TestGenerate(t *testing.T) {
err := Generate()
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,12 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package goman
import "time"
type Instance struct {
Id uint64
CreatedTime time.Time
File string
Line int
}

View File

@@ -0,0 +1,81 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package goman
import (
"runtime"
"sync"
"time"
)
var locker = &sync.Mutex{}
var instanceMap = map[uint64]*Instance{} // id => *Instance
var instanceId = uint64(0)
// New 新创建goroutine
func New(f func()) {
_, file, line, _ := runtime.Caller(1)
go func() {
locker.Lock()
instanceId++
var instance = &Instance{
Id: instanceId,
CreatedTime: time.Now(),
}
instance.File = file
instance.Line = line
instanceMap[instanceId] = instance
locker.Unlock()
// run function
f()
locker.Lock()
delete(instanceMap, instanceId)
locker.Unlock()
}()
}
// NewWithArgs 创建带有参数的goroutine
func NewWithArgs(f func(args ...interface{}), args ...interface{}) {
_, file, line, _ := runtime.Caller(1)
go func() {
locker.Lock()
instanceId++
var instance = &Instance{
Id: instanceId,
CreatedTime: time.Now(),
}
instance.File = file
instance.Line = line
instanceMap[instanceId] = instance
locker.Unlock()
// run function
f(args...)
locker.Lock()
delete(instanceMap, instanceId)
locker.Unlock()
}()
}
// List 列出所有正在运行goroutine
func List() []*Instance {
locker.Lock()
defer locker.Unlock()
var result = []*Instance{}
for _, instance := range instanceMap {
result = append(result, instance)
}
return result
}

View File

@@ -0,0 +1,28 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package goman
import (
"testing"
"time"
)
func TestNew(t *testing.T) {
New(func() {
t.Log("Hello")
t.Log(List())
})
time.Sleep(1 * time.Second)
t.Log(List())
time.Sleep(1 * time.Second)
}
func TestNewWithArgs(t *testing.T) {
NewWithArgs(func(args ...interface{}) {
t.Log(args[0], args[1])
}, 1, 2)
time.Sleep(1 * time.Second)
}

View File

@@ -0,0 +1,476 @@
package nodes
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
"github.com/iwind/gosock/pkg/gosock"
"gopkg.in/yaml.v3"
"log"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
var SharedAdminNode *AdminNode = nil
type AdminNode struct {
sock *gosock.Sock
subPIDs []int
}
func NewAdminNode() *AdminNode {
return &AdminNode{}
}
func (this *AdminNode) Run() {
SharedAdminNode = this
// 启动管理界面
var secret = this.genSecret()
configs.Secret = secret
// 本地Sock
err := this.listenSock()
if err != nil {
logs.Println("[NODE]", err.Error())
return
}
// 检查server配置
err = this.checkServer()
if err != nil {
if err != nil {
logs.Println("[NODE]" + err.Error())
return
}
return
}
// 添加端口到防火墙
this.addPortsToFirewall()
// 监听信号
var sigQueue = make(chan os.Signal, 8)
signal.Notify(sigQueue, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGQUIT)
go func() {
for range sigQueue {
for _, pid := range this.subPIDs {
p, err := os.FindProcess(pid)
if err == nil && p != nil {
_ = p.Kill()
}
}
os.Exit(0)
}
}()
// 触发事件
events.Notify(events.EventStart)
// 启动API节点
this.startAPINode()
// 设置DNS相关
this.setupDNS()
// 启动IP库
this.startIPLibrary()
// 启动Web服务
sessionManager, err := NewSessionManager()
if err != nil {
log.Fatal("start session failed: " + err.Error())
return
}
TeaGo.NewServer(false).
AccessLog(false).
EndAll().
Session(sessionManager, teaconst.CookieSID).
ReadHeaderTimeout(3 * time.Second).
ReadTimeout(1200 * time.Second).
Start()
}
// Daemon 实现守护进程
func (this *AdminNode) Daemon() {
var sock = gosock.NewTmpSock(teaconst.ProcessName)
var isDebug = lists.ContainsString(os.Args, "debug")
for {
conn, err := sock.Dial()
if err != nil {
if isDebug {
log.Println("[DAEMON]starting ...")
}
// 尝试启动
err = func() error {
exe, err := os.Executable()
if err != nil {
return err
}
cmd := exec.Command(exe)
err = cmd.Start()
if err != nil {
return err
}
err = cmd.Wait()
if err != nil {
return err
}
return nil
}()
if err != nil {
if isDebug {
log.Println("[DAEMON]", err)
}
time.Sleep(1 * time.Second)
} else {
time.Sleep(5 * time.Second)
}
} else {
_ = conn.Close()
time.Sleep(5 * time.Second)
}
}
}
// InstallSystemService 安装系统服务
func (this *AdminNode) InstallSystemService() error {
shortName := teaconst.SystemdServiceName
exe, err := os.Executable()
if err != nil {
return err
}
manager := utils.NewServiceManager(shortName, teaconst.ProductName)
err = manager.Install(exe, []string{})
if err != nil {
return err
}
return nil
}
// AddSubPID 添加子PID
func (this *AdminNode) AddSubPID(pid int) {
this.subPIDs = append(this.subPIDs, pid)
}
// 检查Server配置
func (this *AdminNode) checkServer() error {
configFile := Tea.ConfigFile("server.yaml")
_, err := os.Stat(configFile)
if err == nil {
return nil
}
if os.IsNotExist(err) {
// 创建文件
templateFile := Tea.ConfigFile("server.template.yaml")
data, err := os.ReadFile(templateFile)
if err == nil {
err = os.WriteFile(configFile, data, 0666)
if err != nil {
return fmt.Errorf("create config file failed: %w", err)
}
} else {
templateYAML := `# environment code
env: prod
# http
http:
"on": true
listen: [ "0.0.0.0:7788" ]
# https
https:
"on": false
listen: [ "0.0.0.0:443"]
cert: ""
key: ""
`
err = os.WriteFile(configFile, []byte(templateYAML), 0666)
if err != nil {
return fmt.Errorf("create config file failed: %w", err)
}
}
} else {
return fmt.Errorf("can not read config from 'configs/server.yaml': %w", err)
}
return nil
}
// 添加端口到防火墙
func (this *AdminNode) addPortsToFirewall() {
var configFile = Tea.ConfigFile("server.yaml")
data, err := os.ReadFile(configFile)
if err != nil {
return
}
var config = &TeaGo.ServerConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return
}
var ports = []int{}
if config.Http.On {
for _, listen := range config.Http.Listen {
_, portString, _ := net.SplitHostPort(listen)
var port = types.Int(portString)
if port > 0 && !lists.ContainsInt(ports, port) {
ports = append(ports, port)
}
}
}
if config.Https.On {
for _, listen := range config.Https.Listen {
_, portString, _ := net.SplitHostPort(listen)
var port = types.Int(portString)
if port > 0 && !lists.ContainsInt(ports, port) {
ports = append(ports, port)
}
}
}
utils.AddPortsToFirewall(ports)
}
// 设置DNS相关
func (this *AdminNode) setupDNS() {
config, loadErr := configloaders.LoadAdminUIConfig()
if loadErr != nil {
// 默认使用go原生
err := os.Setenv("GODEBUG", "netdns=go")
if err != nil {
logs.Println("[DNS_RESOLVER]set env failed: " + err.Error())
}
return
}
var err error
switch config.DNSResolver.Type {
case nodeconfigs.DNSResolverTypeGoNative:
err = os.Setenv("GODEBUG", "netdns=go")
case nodeconfigs.DNSResolverTypeCGO:
err = os.Setenv("GODEBUG", "netdns=cgo")
default:
// 默认使用go原生
err = os.Setenv("GODEBUG", "netdns=go")
}
if err != nil {
logs.Println("[DNS_RESOLVER]set env failed: " + err.Error())
}
}
// 启动API节点
func (this *AdminNode) startAPINode() {
var configPath = Tea.Root + "/edge-api/configs/api.yaml"
_, err := os.Stat(configPath)
canStart := false
if err == nil {
canStart = true
} else if err != nil && os.IsNotExist(err) {
// 尝试恢复api.yaml
homeDir, _ := os.UserHomeDir()
paths := []string{}
if len(homeDir) > 0 {
paths = append(paths, homeDir+"/.edge-api/api.yaml")
}
paths = append(paths, "/etc/edge-api/api.yaml")
for _, path := range paths {
_, err = os.Stat(path)
if err == nil {
data, err := os.ReadFile(path)
if err == nil {
err = os.WriteFile(configPath, data, 0666)
if err == nil {
logs.Println("[NODE]recover 'edge-api/configs/api.yaml' from '" + path + "'")
canStart = true
break
}
}
}
}
}
dbPath := Tea.Root + "/edge-api/configs/db.yaml"
_, err = os.Stat(dbPath)
if err != nil && os.IsNotExist(err) {
// 尝试恢复db.yaml
homeDir, _ := os.UserHomeDir()
paths := []string{}
if len(homeDir) > 0 {
paths = append(paths, homeDir+"/.edge-api/db.yaml")
}
paths = append(paths, "/etc/edge-api/db.yaml")
for _, path := range paths {
_, err = os.Stat(path)
if err == nil {
data, err := os.ReadFile(path)
if err == nil {
err = os.WriteFile(dbPath, data, 0666)
if err == nil {
logs.Println("[NODE]recover 'edge-api/configs/db.yaml' from '" + path + "'")
break
}
}
}
}
}
if canStart {
logs.Println("start edge-api")
cmd := exec.Command(Tea.Root + "/edge-api/bin/edge-api")
err = cmd.Start()
if err != nil {
logs.Println("[ERROR]start edge-api failed: " + err.Error())
} else {
this.subPIDs = append(this.subPIDs, cmd.Process.Pid)
// 在后台 goroutine 中等待子进程退出,避免僵尸进程
go func() {
waitErr := cmd.Wait()
if waitErr != nil {
logs.Println("[NODE]edge-api process exited with error: " + waitErr.Error())
} else {
logs.Println("[NODE]edge-api process exited normally")
}
// 从 subPIDs 中移除已退出的进程
for i, pid := range this.subPIDs {
if pid == cmd.Process.Pid {
this.subPIDs = append(this.subPIDs[:i], this.subPIDs[i+1:]...)
break
}
}
}()
}
}
}
// 生成Secret
func (this *AdminNode) genSecret() string {
tmpFile := os.TempDir() + "/edge-admin-secret.tmp"
data, err := os.ReadFile(tmpFile)
if err == nil && len(data) == 32 {
return string(data)
}
secret := rands.String(32)
_ = os.WriteFile(tmpFile, []byte(secret), 0666)
return secret
}
// 监听本地sock
func (this *AdminNode) listenSock() error {
this.sock = gosock.NewTmpSock(teaconst.ProcessName)
// 检查是否在运行
if this.sock.IsListening() {
reply, err := this.sock.Send(&gosock.Command{Code: "pid"})
if err == nil {
return errors.New("error: the process is already running, pid: " + maps.NewMap(reply.Params).GetString("pid"))
} else {
return errors.New("error: the process is already running")
}
}
// 启动监听
go func() {
this.sock.OnCommand(func(cmd *gosock.Command) {
switch cmd.Code {
case "pid":
_ = cmd.Reply(&gosock.Command{
Code: "pid",
Params: map[string]interface{}{
"pid": os.Getpid(),
},
})
case "stop":
_ = cmd.ReplyOk()
// 关闭子进程
for _, pid := range this.subPIDs {
p, err := os.FindProcess(pid)
if err == nil && p != nil {
_ = p.Kill()
}
}
// 停止当前目录下的API节点
var apiSock = gosock.NewTmpSock("edge-api")
apiReply, err := apiSock.Send(&gosock.Command{Code: "info"})
if err == nil {
adminExe, _ := os.Executable()
if len(adminExe) > 0 && apiReply != nil && strings.HasPrefix(maps.NewMap(apiReply.Params).GetString("path"), filepath.Dir(filepath.Dir(adminExe))) {
_, _ = apiSock.Send(&gosock.Command{Code: "stop"})
}
}
// 退出主进程
events.Notify(events.EventQuit)
os.Exit(0)
case "recover":
teaconst.IsRecoverMode = true
_ = cmd.ReplyOk()
case "demo":
teaconst.IsDemoMode = !teaconst.IsDemoMode
_ = cmd.Reply(&gosock.Command{
Params: map[string]interface{}{"isDemo": teaconst.IsDemoMode},
})
case "info":
exePath, _ := os.Executable()
_ = cmd.Reply(&gosock.Command{
Code: "info",
Params: map[string]interface{}{
"pid": os.Getpid(),
"version": teaconst.Version,
"path": exePath,
},
})
case "dev": // 切换到dev
Tea.Env = Tea.EnvDev
_ = cmd.ReplyOk()
case "prod": // 切换到prod
Tea.Env = Tea.EnvProd
_ = cmd.ReplyOk()
case "security.reset":
var newConfig = configloaders.NewSecurityConfig()
_ = configloaders.UpdateSecurityConfig(newConfig)
_ = cmd.ReplyOk()
}
})
err := this.sock.Listen()
if err != nil {
logs.Println("NODE", err.Error())
}
}()
events.On(events.EventQuit, func() {
logs.Println("NODE", "quit unix sock")
_ = this.sock.Close()
})
return nil
}

View File

@@ -0,0 +1,18 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !plus
package nodes
import (
"github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
"github.com/iwind/TeaGo/logs"
)
// 启动IP库
func (this *AdminNode) startIPLibrary() {
logs.Println("[NODE]initializing ip library ...")
err := iplibrary.InitDefault()
if err != nil {
logs.Println("[NODE]initialize ip library failed: " + err.Error())
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package nodes
import (
"github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
"github.com/iwind/TeaGo/logs"
)
// 启动IP库
func (this *AdminNode) startIPLibrary() {
logs.Println("[NODE]initializing ip library ...")
err := iplibrary.InitDefault()
if err != nil {
logs.Println("[NODE]initialize ip library failed: " + err.Error())
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package nodes
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/ttlcache"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"strings"
"time"
)
// SessionManager SESSION管理
type SessionManager struct {
life uint
}
func NewSessionManager() (*SessionManager, error) {
return &SessionManager{}, nil
}
func (this *SessionManager) Init(config *actions.SessionConfig) {
this.life = config.Life
}
func (this *SessionManager) Read(sid string) map[string]string {
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return map[string]string{}
}
var result = map[string]string{}
var cacheKey = "SESSION@" + sid
var item = ttlcache.DefaultCache.Read(cacheKey)
if item != nil && item.Value != nil {
itemMap, ok := item.Value.(map[string]string)
if ok {
return itemMap
}
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return map[string]string{}
}
resp, err := rpcClient.LoginSessionRPC().FindLoginSession(rpcClient.Context(0), &pb.FindLoginSessionRequest{Sid: sid})
if err != nil {
logs.Println("SESSION", "read '"+sid+"' failed: "+err.Error())
result["@error"] = err.Error()
return result
}
var session = resp.LoginSession
if session == nil || len(session.ValuesJSON) == 0 {
return result
}
err = json.Unmarshal(session.ValuesJSON, &result)
if err != nil {
logs.Println("SESSION", "decode '"+sid+"' values failed: "+err.Error())
}
// Write to cache
ttlcache.DefaultCache.Write(cacheKey, result, time.Now().Unix()+300 /** must not be too long **/)
return result
}
func (this *SessionManager) WriteItem(sid string, key string, value string) bool {
// 删除缓存
defer ttlcache.DefaultCache.Delete("SESSION@" + sid)
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return false
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false
}
_, err = rpcClient.LoginSessionRPC().WriteLoginSessionValue(rpcClient.Context(0), &pb.WriteLoginSessionValueRequest{
Sid: sid,
Key: key,
Value: value,
})
if err != nil {
logs.Println("SESSION", "write sid:'"+sid+"' key:'"+key+"' failed: "+err.Error())
}
return true
}
func (this *SessionManager) Delete(sid string) bool {
// 删除缓存
defer ttlcache.DefaultCache.Delete("SESSION@" + sid)
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return false
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false
}
_, err = rpcClient.LoginSessionRPC().DeleteLoginSession(rpcClient.Context(0), &pb.DeleteLoginSessionRequest{Sid: sid})
if err != nil {
logs.Println("SESSION", "delete '"+sid+"' failed: "+err.Error())
}
return true
}

View File

@@ -0,0 +1,10 @@
package oplogs
const (
LevelNone = "none"
LevelInfo = "info"
LevelDebug = "debug"
LevelWarn = "warn"
LevelError = "error"
LevelFatal = "fatal"
)

View File

@@ -0,0 +1,89 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package plus
type ComponentCode = string
const (
ComponentCodeUser ComponentCode = "user"
ComponentCodeScheduling ComponentCode = "scheduling"
ComponentCodeMonitor ComponentCode = "monitor"
ComponentCodeLog ComponentCode = "log"
ComponentCodeReporter ComponentCode = "reporter"
ComponentCodePlan ComponentCode = "plan"
ComponentCodeFinance ComponentCode = "finance"
ComponentCodeNS ComponentCode = "ns"
ComponentCodeL2Node ComponentCode = "l2node"
ComponentCodeComputing ComponentCode = "computing"
ComponentCodeTicket ComponentCode = "ticket"
ComponentCodeAntiDDoS ComponentCode = "antiDDoS"
)
type ComponentDefinition struct {
Name string `json:"name"`
Code ComponentCode `json:"code"`
Description string `json:"description"`
}
func FindAllComponents() []*ComponentDefinition {
return []*ComponentDefinition{
{
Name: "多租户",
Code: ComponentCodeUser,
},
{
Name: "智能调度",
Code: ComponentCodeScheduling,
},
{
Name: "监控",
Code: ComponentCodeMonitor,
},
{
Name: "日志",
Code: ComponentCodeLog,
},
{
Name: "区域监控",
Code: ComponentCodeReporter,
},
{
Name: "套餐",
Code: ComponentCodePlan,
},
{
Name: "财务",
Code: ComponentCodeFinance,
},
{
Name: "智能DNS",
Code: ComponentCodeNS,
},
{
Name: "L2节点",
Code: ComponentCodeL2Node,
},
{
Name: "边缘计算",
Code: ComponentCodeComputing,
},
{
Name: "工单系统",
Code: ComponentCodeTicket,
},
{
Name: "高防IP",
Code: ComponentCodeAntiDDoS,
},
}
}
func CheckComponent(code string) bool {
for _, c := range FindAllComponents() {
if c.Code == code {
return true
}
}
return false
}

View File

@@ -0,0 +1,6 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package plus
var ErrString = ""

View File

@@ -0,0 +1,51 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package plus
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/actions"
"net/http"
)
func NewHelper(component ComponentCode) *Helper {
return &Helper{component: component}
}
func NewBasicHelper() *Helper {
return NewHelper("")
}
type Helper struct {
component string
}
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
if !teaconst.IsPlus || !AllowComponent(this.component) {
var writer = actionPtr.Object().ResponseWriter
writer.WriteHeader(http.StatusForbidden)
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = writer.Write([]byte(`<!DOCTYPE html>
<html>
<head>
<title>权限错误</title>
<link rel="stylesheet" type="text/css" href="/css/semantic.min.css?v=bRafhK" media="all"/>
<style type="text/css">
div { max-width: 960px; margin: 4em auto; }
h3 { font-weight: normal; }
p { font-size: 14px; }
</style>
</head>
<body>
<div>
<h3>权限错误</h3>
<p>此操作需要商业版用户权限。如果你已经是商业用户,请重新访问 <a href="/settings/authority">商业版认证页</a> 以便激活权限。</p>
</div>
</body>
</html>`))
return false
}
return true
}

View File

@@ -0,0 +1,98 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package plus
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
plusutils "github.com/TeaOSLab/EdgePlus/pkg/utils"
"github.com/iwind/TeaGo/lists"
"net"
"sync"
)
// 已经开通的组件
var plusLocker = sync.Mutex{}
var plusComponents = []string{}
var PlusEdition = ""
// UpdateComponents 修改开通的组件
func UpdateComponents(edition string, components []string) {
plusLocker.Lock()
PlusEdition = edition
plusComponents = components
plusLocker.Unlock()
}
// AllowComponent 检查是否允许使用某个组件
func AllowComponent(component ComponentCode) bool {
if !teaconst.IsPlus {
return false
}
// 允许不指定组件表示只要是plus用户就可以
if len(component) == 0 {
return true
}
plusLocker.Lock()
defer plusLocker.Unlock()
if len(plusComponents) == 0 {
return false
}
for _, c := range plusComponents {
if c == "*" || c == component {
// 如果不是最新版本的授权,则忽略一些权限
if c == "*" && (len(PlusEdition) == 0 || plusutils.CompareEdition(PlusEdition, plusutils.EditionPro) < 0) && component == ComponentCodeAntiDDoS {
continue
}
return true
}
}
return false
}
// ValidateMac 校验MAC地址
func ValidateMac(mac string) bool {
if len(mac) == 0 || mac == "*" {
return true
}
for _, addr := range FindAllMacAddresses() {
if addr == mac {
return true
}
}
return false
}
// FindAllMacAddresses 获取MAC地址
func FindAllMacAddresses() []string {
var macAddresses = []string{}
interfaces, _ := net.Interfaces()
for _, i := range interfaces {
var addr = i.HardwareAddr.String()
if len(addr) == 0 {
continue
}
if lists.ContainsString(macAddresses, addr) {
continue
}
if i.Flags&net.FlagLoopback == 1 {
continue
}
if i.Flags&net.FlagUp == 0 {
continue
}
ipAddrs, _ := i.Addrs()
if len(ipAddrs) == 0 {
continue
}
macAddresses = append(macAddresses, addr)
}
return macAddresses
}

View File

@@ -0,0 +1,621 @@
package rpc
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/encrypt"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/encoding/gzip"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
"net"
"net/url"
"strings"
"sync"
"time"
)
// RPCClient RPC客户端
type RPCClient struct {
apiConfig *configs.APIConfig
conns []*grpc.ClientConn
locker sync.RWMutex
}
// NewRPCClient 构造新的RPC客户端
func NewRPCClient(apiConfig *configs.APIConfig, isPrimary bool) (*RPCClient, error) {
if apiConfig == nil {
return nil, errors.New("api config should not be nil")
}
var client = &RPCClient{
apiConfig: apiConfig,
}
err := client.init()
if err != nil {
return nil, err
}
// 设置RPC
if isPrimary {
dao.SetRPC(client)
}
return client, nil
}
func (this *RPCClient) APITokenRPC() pb.APITokenServiceClient {
return pb.NewAPITokenServiceClient(this.pickConn())
}
func (this *RPCClient) AdminRPC() pb.AdminServiceClient {
return pb.NewAdminServiceClient(this.pickConn())
}
func (this *RPCClient) NodeRPC() pb.NodeServiceClient {
return pb.NewNodeServiceClient(this.pickConn())
}
func (this *RPCClient) NodeLogRPC() pb.NodeLogServiceClient {
return pb.NewNodeLogServiceClient(this.pickConn())
}
func (this *RPCClient) NodeGrantRPC() pb.NodeGrantServiceClient {
return pb.NewNodeGrantServiceClient(this.pickConn())
}
func (this *RPCClient) NodeLoginRPC() pb.NodeLoginServiceClient {
return pb.NewNodeLoginServiceClient(this.pickConn())
}
func (this *RPCClient) NodeClusterRPC() pb.NodeClusterServiceClient {
return pb.NewNodeClusterServiceClient(this.pickConn())
}
func (this *RPCClient) NodeClusterFirewallActionRPC() pb.NodeClusterFirewallActionServiceClient {
return pb.NewNodeClusterFirewallActionServiceClient(this.pickConn())
}
func (this *RPCClient) NodeGroupRPC() pb.NodeGroupServiceClient {
return pb.NewNodeGroupServiceClient(this.pickConn())
}
func (this *RPCClient) NodeRegionRPC() pb.NodeRegionServiceClient {
return pb.NewNodeRegionServiceClient(this.pickConn())
}
func (this *RPCClient) NodeIPAddressRPC() pb.NodeIPAddressServiceClient {
return pb.NewNodeIPAddressServiceClient(this.pickConn())
}
func (this *RPCClient) NodeIPAddressLogRPC() pb.NodeIPAddressLogServiceClient {
return pb.NewNodeIPAddressLogServiceClient(this.pickConn())
}
func (this *RPCClient) NodeIPAddressThresholdRPC() pb.NodeIPAddressThresholdServiceClient {
return pb.NewNodeIPAddressThresholdServiceClient(this.pickConn())
}
func (this *RPCClient) NodeValueRPC() pb.NodeValueServiceClient {
return pb.NewNodeValueServiceClient(this.pickConn())
}
func (this *RPCClient) NodeThresholdRPC() pb.NodeThresholdServiceClient {
return pb.NewNodeThresholdServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRPC() pb.ServerServiceClient {
return pb.NewServerServiceClient(this.pickConn())
}
func (this *RPCClient) ServerBandwidthStatRPC() pb.ServerBandwidthStatServiceClient {
return pb.NewServerBandwidthStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerClientSystemMonthlyStatRPC() pb.ServerClientSystemMonthlyStatServiceClient {
return pb.NewServerClientSystemMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerClientBrowserMonthlyStatRPC() pb.ServerClientBrowserMonthlyStatServiceClient {
return pb.NewServerClientBrowserMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionCountryMonthlyStatRPC() pb.ServerRegionCountryMonthlyStatServiceClient {
return pb.NewServerRegionCountryMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionProvinceMonthlyStatRPC() pb.ServerRegionProvinceMonthlyStatServiceClient {
return pb.NewServerRegionProvinceMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionCityMonthlyStatRPC() pb.ServerRegionCityMonthlyStatServiceClient {
return pb.NewServerRegionCityMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionProviderMonthlyStatRPC() pb.ServerRegionProviderMonthlyStatServiceClient {
return pb.NewServerRegionProviderMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerHTTPFirewallDailyStatRPC() pb.ServerHTTPFirewallDailyStatServiceClient {
return pb.NewServerHTTPFirewallDailyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerDailyStatRPC() pb.ServerDailyStatServiceClient {
return pb.NewServerDailyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerGroupRPC() pb.ServerGroupServiceClient {
return pb.NewServerGroupServiceClient(this.pickConn())
}
func (this *RPCClient) APINodeRPC() pb.APINodeServiceClient {
return pb.NewAPINodeServiceClient(this.pickConn())
}
func (this *RPCClient) APIMethodStatRPC() pb.APIMethodStatServiceClient {
return pb.NewAPIMethodStatServiceClient(this.pickConn())
}
func (this *RPCClient) DBNodeRPC() pb.DBNodeServiceClient {
return pb.NewDBNodeServiceClient(this.pickConn())
}
func (this *RPCClient) DBRPC() pb.DBServiceClient {
return pb.NewDBServiceClient(this.pickConn())
}
func (this *RPCClient) OriginRPC() pb.OriginServiceClient {
return pb.NewOriginServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPWebRPC() pb.HTTPWebServiceClient {
return pb.NewHTTPWebServiceClient(this.pickConn())
}
func (this *RPCClient) ReverseProxyRPC() pb.ReverseProxyServiceClient {
return pb.NewReverseProxyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPGzipRPC() pb.HTTPGzipServiceClient {
return pb.NewHTTPGzipServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPHeaderRPC() pb.HTTPHeaderServiceClient {
return pb.NewHTTPHeaderServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPHeaderPolicyRPC() pb.HTTPHeaderPolicyServiceClient {
return pb.NewHTTPHeaderPolicyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPPageRPC() pb.HTTPPageServiceClient {
return pb.NewHTTPPageServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPAccessLogPolicyRPC() pb.HTTPAccessLogPolicyServiceClient {
return pb.NewHTTPAccessLogPolicyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPCachePolicyRPC() pb.HTTPCachePolicyServiceClient {
return pb.NewHTTPCachePolicyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPCacheTaskRPC() pb.HTTPCacheTaskServiceClient {
return pb.NewHTTPCacheTaskServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPCacheTaskKeyRPC() pb.HTTPCacheTaskKeyServiceClient {
return pb.NewHTTPCacheTaskKeyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPFirewallPolicyRPC() pb.HTTPFirewallPolicyServiceClient {
return pb.NewHTTPFirewallPolicyServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPFirewallRuleGroupRPC() pb.HTTPFirewallRuleGroupServiceClient {
return pb.NewHTTPFirewallRuleGroupServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPFirewallRuleSetRPC() pb.HTTPFirewallRuleSetServiceClient {
return pb.NewHTTPFirewallRuleSetServiceClient(this.pickConn())
}
func (this *RPCClient) FirewallRPC() pb.FirewallServiceClient {
return pb.NewFirewallServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPLocationRPC() pb.HTTPLocationServiceClient {
return pb.NewHTTPLocationServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPWebsocketRPC() pb.HTTPWebsocketServiceClient {
return pb.NewHTTPWebsocketServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPRewriteRuleRPC() pb.HTTPRewriteRuleServiceClient {
return pb.NewHTTPRewriteRuleServiceClient(this.pickConn())
}
// HTTPAccessLogRPC 访问日志
func (this *RPCClient) HTTPAccessLogRPC() pb.HTTPAccessLogServiceClient {
return pb.NewHTTPAccessLogServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPFastcgiRPC() pb.HTTPFastcgiServiceClient {
return pb.NewHTTPFastcgiServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPAuthPolicyRPC() pb.HTTPAuthPolicyServiceClient {
return pb.NewHTTPAuthPolicyServiceClient(this.pickConn())
}
func (this *RPCClient) SSLCertRPC() pb.SSLCertServiceClient {
return pb.NewSSLCertServiceClient(this.pickConn())
}
func (this *RPCClient) SSLPolicyRPC() pb.SSLPolicyServiceClient {
return pb.NewSSLPolicyServiceClient(this.pickConn())
}
func (this *RPCClient) SysSettingRPC() pb.SysSettingServiceClient {
return pb.NewSysSettingServiceClient(this.pickConn())
}
func (this *RPCClient) MessageRPC() pb.MessageServiceClient {
return pb.NewMessageServiceClient(this.pickConn())
}
func (this *RPCClient) IPLibraryRPC() pb.IPLibraryServiceClient {
return pb.NewIPLibraryServiceClient(this.pickConn())
}
func (this *RPCClient) IPLibraryFileRPC() pb.IPLibraryFileServiceClient {
return pb.NewIPLibraryFileServiceClient(this.pickConn())
}
func (this *RPCClient) IPLibraryArtifactRPC() pb.IPLibraryArtifactServiceClient {
return pb.NewIPLibraryArtifactServiceClient(this.pickConn())
}
func (this *RPCClient) IPListRPC() pb.IPListServiceClient {
return pb.NewIPListServiceClient(this.pickConn())
}
func (this *RPCClient) IPItemRPC() pb.IPItemServiceClient {
return pb.NewIPItemServiceClient(this.pickConn())
}
func (this *RPCClient) FileRPC() pb.FileServiceClient {
return pb.NewFileServiceClient(this.pickConn())
}
func (this *RPCClient) FileChunkRPC() pb.FileChunkServiceClient {
return pb.NewFileChunkServiceClient(this.pickConn())
}
func (this *RPCClient) RegionCountryRPC() pb.RegionCountryServiceClient {
return pb.NewRegionCountryServiceClient(this.pickConn())
}
func (this *RPCClient) RegionProvinceRPC() pb.RegionProvinceServiceClient {
return pb.NewRegionProvinceServiceClient(this.pickConn())
}
func (this *RPCClient) RegionCityRPC() pb.RegionCityServiceClient {
return pb.NewRegionCityServiceClient(this.pickConn())
}
func (this *RPCClient) RegionTownRPC() pb.RegionTownServiceClient {
return pb.NewRegionTownServiceClient(this.pickConn())
}
func (this *RPCClient) RegionProviderRPC() pb.RegionProviderServiceClient {
return pb.NewRegionProviderServiceClient(this.pickConn())
}
func (this *RPCClient) LogRPC() pb.LogServiceClient {
return pb.NewLogServiceClient(this.pickConn())
}
func (this *RPCClient) DNSProviderRPC() pb.DNSProviderServiceClient {
return pb.NewDNSProviderServiceClient(this.pickConn())
}
func (this *RPCClient) DNSDomainRPC() pb.DNSDomainServiceClient {
return pb.NewDNSDomainServiceClient(this.pickConn())
}
func (this *RPCClient) DNSRPC() pb.DNSServiceClient {
return pb.NewDNSServiceClient(this.pickConn())
}
func (this *RPCClient) DNSTaskRPC() pb.DNSTaskServiceClient {
return pb.NewDNSTaskServiceClient(this.pickConn())
}
func (this *RPCClient) ACMEUserRPC() pb.ACMEUserServiceClient {
return pb.NewACMEUserServiceClient(this.pickConn())
}
func (this *RPCClient) ACMETaskRPC() pb.ACMETaskServiceClient {
return pb.NewACMETaskServiceClient(this.pickConn())
}
func (this *RPCClient) ACMEProviderRPC() pb.ACMEProviderServiceClient {
return pb.NewACMEProviderServiceClient(this.pickConn())
}
func (this *RPCClient) ACMEProviderAccountRPC() pb.ACMEProviderAccountServiceClient {
return pb.NewACMEProviderAccountServiceClient(this.pickConn())
}
func (this *RPCClient) UserRPC() pb.UserServiceClient {
return pb.NewUserServiceClient(this.pickConn())
}
func (this *RPCClient) UserAccessKeyRPC() pb.UserAccessKeyServiceClient {
return pb.NewUserAccessKeyServiceClient(this.pickConn())
}
func (this *RPCClient) UserIdentityRPC() pb.UserIdentityServiceClient {
return pb.NewUserIdentityServiceClient(this.pickConn())
}
func (this *RPCClient) LoginRPC() pb.LoginServiceClient {
return pb.NewLoginServiceClient(this.pickConn())
}
func (this *RPCClient) LoginSessionRPC() pb.LoginSessionServiceClient {
return pb.NewLoginSessionServiceClient(this.pickConn())
}
func (this *RPCClient) LoginTicketRPC() pb.LoginTicketServiceClient {
return pb.NewLoginTicketServiceClient(this.pickConn())
}
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
return pb.NewNodeTaskServiceClient(this.pickConn())
}
func (this *RPCClient) LatestItemRPC() pb.LatestItemServiceClient {
return pb.NewLatestItemServiceClient(this.pickConn())
}
func (this *RPCClient) MetricItemRPC() pb.MetricItemServiceClient {
return pb.NewMetricItemServiceClient(this.pickConn())
}
func (this *RPCClient) MetricStatRPC() pb.MetricStatServiceClient {
return pb.NewMetricStatServiceClient(this.pickConn())
}
func (this *RPCClient) MetricChartRPC() pb.MetricChartServiceClient {
return pb.NewMetricChartServiceClient(this.pickConn())
}
func (this *RPCClient) NodeClusterMetricItemRPC() pb.NodeClusterMetricItemServiceClient {
return pb.NewNodeClusterMetricItemServiceClient(this.pickConn())
}
func (this *RPCClient) ServerStatBoardRPC() pb.ServerStatBoardServiceClient {
return pb.NewServerStatBoardServiceClient(this.pickConn())
}
func (this *RPCClient) ServerDomainHourlyStatRPC() pb.ServerDomainHourlyStatServiceClient {
return pb.NewServerDomainHourlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerStatBoardChartRPC() pb.ServerStatBoardChartServiceClient {
return pb.NewServerStatBoardChartServiceClient(this.pickConn())
}
func (this *RPCClient) TrafficDailyStatRPC() pb.TrafficDailyStatServiceClient {
return pb.NewTrafficDailyStatServiceClient(this.pickConn())
}
// Context 构造Admin上下文
func (this *RPCClient) Context(adminId int64) context.Context {
var ctx = context.Background()
var m = maps.Map{
"timestamp": time.Now().Unix(),
"type": "admin",
"userId": adminId,
}
method, err := encrypt.NewMethodInstance(teaconst.EncryptMethod, this.apiConfig.Secret, this.apiConfig.NodeId)
if err != nil {
utils.PrintError(err)
return context.Background()
}
data, err := method.Encrypt(m.AsJSON())
if err != nil {
utils.PrintError(err)
return context.Background()
}
var token = base64.StdEncoding.EncodeToString(data)
ctx = metadata.AppendToOutgoingContext(ctx, "nodeId", this.apiConfig.NodeId, "token", token)
return ctx
}
// APIContext 构造API上下文
func (this *RPCClient) APIContext(apiNodeId int64) context.Context {
var ctx = context.Background()
var m = maps.Map{
"timestamp": time.Now().Unix(),
"type": "api",
"userId": apiNodeId,
}
method, err := encrypt.NewMethodInstance(teaconst.EncryptMethod, this.apiConfig.Secret, this.apiConfig.NodeId)
if err != nil {
utils.PrintError(err)
return context.Background()
}
data, err := method.Encrypt(m.AsJSON())
if err != nil {
utils.PrintError(err)
return context.Background()
}
var token = base64.StdEncoding.EncodeToString(data)
ctx = metadata.AppendToOutgoingContext(ctx, "nodeId", this.apiConfig.NodeId, "token", token)
return ctx
}
// UpdateConfig 修改配置
func (this *RPCClient) UpdateConfig(config *configs.APIConfig) error {
this.apiConfig = config
this.locker.Lock()
err := this.init()
this.locker.Unlock()
return err
}
// 初始化
func (this *RPCClient) init() error {
// 当前的IP地址
var localIPAddrs = this.localIPAddrs()
// 重新连接
var conns = []*grpc.ClientConn{}
for _, endpoint := range this.apiConfig.RPCEndpoints {
u, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("parse endpoint failed: %w", err)
}
var apiHost = u.Host
// 如果本机,则将地址修改为回路地址
if lists.ContainsString(localIPAddrs, u.Hostname()) {
if strings.Contains(apiHost, "[") { // IPv6 [host]:port
apiHost = "[::1]"
} else {
apiHost = "127.0.0.1"
}
var port = u.Port()
if len(port) > 0 {
apiHost += ":" + port
}
}
var conn *grpc.ClientConn
var callOptions = grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(128<<20),
grpc.MaxCallSendMsgSize(128<<20),
grpc.UseCompressor(gzip.Name),
)
var keepaliveParams = grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second,
})
if u.Scheme == "http" {
conn, err = grpc.Dial(apiHost, grpc.WithTransportCredentials(insecure.NewCredentials()), callOptions, keepaliveParams)
} else if u.Scheme == "https" {
conn, err = grpc.Dial(apiHost, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
InsecureSkipVerify: true,
})), callOptions, keepaliveParams)
} else {
return errors.New("parse endpoint failed: invalid scheme '" + u.Scheme + "'")
}
if err != nil {
return err
}
conns = append(conns, conn)
}
if len(conns) == 0 {
return errors.New("[RPC]no available endpoints")
}
// 这里不需要加锁因为会和pickConn冲突
this.conns = conns
return nil
}
// 随机选择一个连接
func (this *RPCClient) pickConn() *grpc.ClientConn {
this.locker.Lock()
defer this.locker.Unlock()
// 检查连接状态
var countConns = len(this.conns)
if countConns > 0 {
if countConns == 1 {
return this.conns[0]
}
for _, state := range []connectivity.State{
connectivity.Ready,
connectivity.Idle,
connectivity.Connecting,
connectivity.TransientFailure,
} {
var availableConns = []*grpc.ClientConn{}
for _, conn := range this.conns {
if conn.GetState() == state {
availableConns = append(availableConns, conn)
}
}
if len(availableConns) > 0 {
return this.randConn(availableConns)
}
}
}
return this.randConn(this.conns)
}
// Close 关闭
func (this *RPCClient) Close() error {
this.locker.Lock()
defer this.locker.Unlock()
var lastErr error
for _, conn := range this.conns {
var err = conn.Close()
if err != nil {
lastErr = err
continue
}
}
return lastErr
}
func (this *RPCClient) localIPAddrs() []string {
localInterfaceAddrs, err := net.InterfaceAddrs()
var localIPAddrs = []string{}
if err == nil {
for _, addr := range localInterfaceAddrs {
var addrString = addr.String()
var index = strings.Index(addrString, "/")
if index > 0 {
localIPAddrs = append(localIPAddrs, addrString[:index])
}
}
}
return localIPAddrs
}
func (this *RPCClient) randConn(conns []*grpc.ClientConn) *grpc.ClientConn {
var l = len(conns)
if l == 0 {
return nil
}
if l == 1 {
return conns[0]
}
return conns[rands.Int(0, l-1)]
}

View File

@@ -0,0 +1,250 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package rpc
import "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
func (this *RPCClient) ScriptRPC() pb.ScriptServiceClient {
return pb.NewScriptServiceClient(this.pickConn())
}
func (this *RPCClient) UserScriptRPC() pb.UserScriptServiceClient {
return pb.NewUserScriptServiceClient(this.pickConn())
}
func (this *RPCClient) UserOrderRPC() pb.UserOrderServiceClient {
return pb.NewUserOrderServiceClient(this.pickConn())
}
func (this *RPCClient) OrderMethodRPC() pb.OrderMethodServiceClient {
return pb.NewOrderMethodServiceClient(this.pickConn())
}
func (this *RPCClient) UserTicketRPC() pb.UserTicketServiceClient {
return pb.NewUserTicketServiceClient(this.pickConn())
}
func (this *RPCClient) UserTicketLogRPC() pb.UserTicketLogServiceClient {
return pb.NewUserTicketLogServiceClient(this.pickConn())
}
func (this *RPCClient) UserTicketCategoryRPC() pb.UserTicketCategoryServiceClient {
return pb.NewUserTicketCategoryServiceClient(this.pickConn())
}
func (this *RPCClient) NSDomainRPC() pb.NSDomainServiceClient {
return pb.NewNSDomainServiceClient(this.pickConn())
}
func (this *RPCClient) NSDomainGroupRPC() pb.NSDomainGroupServiceClient {
return pb.NewNSDomainGroupServiceClient(this.pickConn())
}
func (this *RPCClient) NSRecordRPC() pb.NSRecordServiceClient {
return pb.NewNSRecordServiceClient(this.pickConn())
}
func (this *RPCClient) NSKeyRPC() pb.NSKeyServiceClient {
return pb.NewNSKeyServiceClient(this.pickConn())
}
func (this *RPCClient) NSRouteRPC() pb.NSRouteServiceClient {
return pb.NewNSRouteServiceClient(this.pickConn())
}
func (this *RPCClient) NSRouteCategoryRPC() pb.NSRouteCategoryServiceClient {
return pb.NewNSRouteCategoryServiceClient(this.pickConn())
}
func (this *RPCClient) NSAccessLogRPC() pb.NSAccessLogServiceClient {
return pb.NewNSAccessLogServiceClient(this.pickConn())
}
func (this *RPCClient) NSRPC() pb.NSServiceClient {
return pb.NewNSServiceClient(this.pickConn())
}
func (this *RPCClient) NSQuestionOptionRPC() pb.NSQuestionOptionServiceClient {
return pb.NewNSQuestionOptionServiceClient(this.pickConn())
}
func (this *RPCClient) NSClusterRPC() pb.NSClusterServiceClient {
return pb.NewNSClusterServiceClient(this.pickConn())
}
func (this *RPCClient) NSNodeRPC() pb.NSNodeServiceClient {
return pb.NewNSNodeServiceClient(this.pickConn())
}
func (this *RPCClient) NSRecordHourlyStatRPC() pb.NSRecordHourlyStatServiceClient {
return pb.NewNSRecordHourlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) NSPlanRPC() pb.NSPlanServiceClient {
return pb.NewNSPlanServiceClient(this.pickConn())
}
func (this *RPCClient) NSUserPlanRPC() pb.NSUserPlanServiceClient {
return pb.NewNSUserPlanServiceClient(this.pickConn())
}
func (this *RPCClient) ReportNodeRPC() pb.ReportNodeServiceClient {
return pb.NewReportNodeServiceClient(this.pickConn())
}
func (this *RPCClient) ReportNodeGroupRPC() pb.ReportNodeGroupServiceClient {
return pb.NewReportNodeGroupServiceClient(this.pickConn())
}
func (this *RPCClient) ReportResultRPC() pb.ReportResultServiceClient {
return pb.NewReportResultServiceClient(this.pickConn())
}
func (this *RPCClient) UserNodeRPC() pb.UserNodeServiceClient {
return pb.NewUserNodeServiceClient(this.pickConn())
}
func (this *RPCClient) UserBillRPC() pb.UserBillServiceClient {
return pb.NewUserBillServiceClient(this.pickConn())
}
func (this *RPCClient) ServerBillRPC() pb.ServerBillServiceClient {
return pb.NewServerBillServiceClient(this.pickConn())
}
func (this *RPCClient) UserTrafficBillRPC() pb.UserTrafficBillServiceClient {
return pb.NewUserTrafficBillServiceClient(this.pickConn())
}
func (this *RPCClient) UserAccountRPC() pb.UserAccountServiceClient {
return pb.NewUserAccountServiceClient(this.pickConn())
}
func (this *RPCClient) UserAccountLogRPC() pb.UserAccountLogServiceClient {
return pb.NewUserAccountLogServiceClient(this.pickConn())
}
func (this *RPCClient) UserAccountDailyStatRPC() pb.UserAccountDailyStatServiceClient {
return pb.NewUserAccountDailyStatServiceClient(this.pickConn())
}
func (this *RPCClient) NodePriceItemRPC() pb.NodePriceItemServiceClient {
return pb.NewNodePriceItemServiceClient(this.pickConn())
}
func (this *RPCClient) PriceRPC() pb.PriceServiceClient {
return pb.NewPriceServiceClient(this.pickConn())
}
func (this *RPCClient) PlanRPC() pb.PlanServiceClient {
return pb.NewPlanServiceClient(this.pickConn())
}
func (this *RPCClient) UserPlanRPC() pb.UserPlanServiceClient {
return pb.NewUserPlanServiceClient(this.pickConn())
}
func (this *RPCClient) TrafficPackageRPC() pb.TrafficPackageServiceClient {
return pb.NewTrafficPackageServiceClient(this.pickConn())
}
func (this *RPCClient) TrafficPackagePeriodRPC() pb.TrafficPackagePeriodServiceClient {
return pb.NewTrafficPackagePeriodServiceClient(this.pickConn())
}
func (this *RPCClient) TrafficPackagePriceRPC() pb.TrafficPackagePriceServiceClient {
return pb.NewTrafficPackagePriceServiceClient(this.pickConn())
}
func (this *RPCClient) UserTrafficPackageRPC() pb.UserTrafficPackageServiceClient {
return pb.NewUserTrafficPackageServiceClient(this.pickConn())
}
func (this *RPCClient) FormalClientSystemRPC() pb.FormalClientSystemServiceClient {
return pb.NewFormalClientSystemServiceClient(this.pickConn())
}
func (this *RPCClient) FormalClientBrowserRPC() pb.FormalClientBrowserServiceClient {
return pb.NewFormalClientBrowserServiceClient(this.pickConn())
}
func (this *RPCClient) ClientAgentRPC() pb.ClientAgentServiceClient {
return pb.NewClientAgentServiceClient(this.pickConn())
}
func (this *RPCClient) ClientAgentIPRPC() pb.ClientAgentIPServiceClient {
return pb.NewClientAgentIPServiceClient(this.pickConn())
}
func (this *RPCClient) ADPackageRPC() pb.ADPackageServiceClient {
return pb.NewADPackageServiceClient(this.pickConn())
}
func (this *RPCClient) ADPackageInstanceRPC() pb.ADPackageInstanceServiceClient {
return pb.NewADPackageInstanceServiceClient(this.pickConn())
}
func (this *RPCClient) ADPackagePeriodRPC() pb.ADPackagePeriodServiceClient {
return pb.NewADPackagePeriodServiceClient(this.pickConn())
}
func (this *RPCClient) ADPackagePriceRPC() pb.ADPackagePriceServiceClient {
return pb.NewADPackagePriceServiceClient(this.pickConn())
}
func (this *RPCClient) ADNetworkRPC() pb.ADNetworkServiceClient {
return pb.NewADNetworkServiceClient(this.pickConn())
}
func (this *RPCClient) UserADInstanceRPC() pb.UserADInstanceServiceClient {
return pb.NewUserADInstanceServiceClient(this.pickConn())
}
func (this *RPCClient) NodeActionRPC() pb.NodeActionServiceClient {
return pb.NewNodeActionServiceClient(this.pickConn())
}
func (this *RPCClient) MessageRecipientGroupRPC() pb.MessageRecipientGroupServiceClient {
return pb.NewMessageRecipientGroupServiceClient(this.pickConn())
}
func (this *RPCClient) MessageRecipientRPC() pb.MessageRecipientServiceClient {
return pb.NewMessageRecipientServiceClient(this.pickConn())
}
func (this *RPCClient) MessageMediaRPC() pb.MessageMediaServiceClient {
return pb.NewMessageMediaServiceClient(this.pickConn())
}
func (this *RPCClient) MessageMediaInstanceRPC() pb.MessageMediaInstanceServiceClient {
return pb.NewMessageMediaInstanceServiceClient(this.pickConn())
}
func (this *RPCClient) MessageTaskRPC() pb.MessageTaskServiceClient {
return pb.NewMessageTaskServiceClient(this.pickConn())
}
func (this *RPCClient) MessageTaskLogRPC() pb.MessageTaskLogServiceClient {
return pb.NewMessageTaskLogServiceClient(this.pickConn())
}
func (this *RPCClient) MessageReceiverRPC() pb.MessageReceiverServiceClient {
return pb.NewMessageReceiverServiceClient(this.pickConn())
}
func (this *RPCClient) AuthorityKeyRPC() pb.AuthorityKeyServiceClient {
return pb.NewAuthorityKeyServiceClient(this.pickConn())
}
func (this *RPCClient) SMSSenderRPC() pb.SMSSenderServiceClient {
return pb.NewSMSSenderServiceClient(this.pickConn())
}
func (this *RPCClient) PostCategoryRPC() pb.PostCategoryServiceClient {
return pb.NewPostCategoryServiceClient(this.pickConn())
}
func (this *RPCClient) PostRPC() pb.PostServiceClient {
return pb.NewPostServiceClient(this.pickConn())
}

View File

@@ -0,0 +1,134 @@
package rpc
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
_ "github.com/iwind/TeaGo/bootstrap"
stringutil "github.com/iwind/TeaGo/utils/string"
"testing"
"time"
)
func TestRPCClient_NodeRPC(t *testing.T) {
before := time.Now()
defer func() {
t.Log(time.Since(before).Seconds()*1000, "ms")
}()
config, err := configs.LoadAPIConfig()
if err != nil {
t.Fatal(err)
}
rpc, err := NewRPCClient(config, true)
if err != nil {
t.Fatal(err)
}
resp, err := rpc.AdminRPC().LoginAdmin(rpc.Context(0), &pb.LoginAdminRequest{
Username: "admin",
Password: stringutil.Md5("123456"),
})
if err != nil {
t.Fatal(err)
}
t.Log(resp)
}
func TestRPC_Dial_HTTP(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPCEndpoints: []string{"https://127.0.0.1:8003"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)
}
resp, err := client.NodeRPC().FindEnabledNode(client.Context(1), &pb.FindEnabledNodeRequest{NodeId: 4})
if err != nil {
t.Fatal(err)
}
t.Log(resp.Node)
}
func TestRPC_Dial_HTTP_2(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPCEndpoints: []string{"https://127.0.0.1:8003"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)
}
resp, err := client.NodeRPC().FindEnabledNode(client.Context(1), &pb.FindEnabledNodeRequest{NodeId: 4})
if err != nil {
t.Fatal(err)
}
t.Log(resp.Node)
}
func TestRPC_Dial_HTTPS(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPCEndpoints: []string{"https://127.0.0.1:8004"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)
}
resp, err := client.NodeRPC().FindEnabledNode(client.Context(1), &pb.FindEnabledNodeRequest{NodeId: 4})
if err != nil {
t.Fatal(err)
}
t.Log(resp.Node)
}
func BenchmarkNewRPCClient(b *testing.B) {
config, err := configs.LoadAPIConfig()
if err != nil {
b.Fatal(err)
}
rpc, err := NewRPCClient(config, true)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, err := rpc.AdminRPC().LoginAdmin(rpc.Context(0), &pb.LoginAdminRequest{
Username: "admin",
Password: stringutil.Md5("123456"),
})
if err != nil {
b.Fatal(err)
}
_ = resp
}
}
func BenchmarkNewRPCClient_2(b *testing.B) {
config, err := configs.LoadAPIConfig()
if err != nil {
b.Fatal(err)
}
rpc, err := NewRPCClient(config, true)
if err != nil {
b.Fatal(err)
}
var conn = rpc.AdminRPC()
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, err := conn.LoginAdmin(rpc.Context(0), &pb.LoginAdminRequest{
Username: "admin",
Password: stringutil.Md5("123456"),
})
if err != nil {
b.Fatal(err)
}
_ = resp
}
}

View File

@@ -0,0 +1,69 @@
package rpc
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"strings"
"sync"
)
var sharedRPC *RPCClient = nil
var locker = &sync.Mutex{}
func SharedRPC() (*RPCClient, error) {
locker.Lock()
defer locker.Unlock()
if sharedRPC != nil {
return sharedRPC, nil
}
config, err := configs.LoadAPIConfig()
if err != nil {
return nil, err
}
client, err := NewRPCClient(config, true)
if err != nil {
return nil, err
}
sharedRPC = client
return sharedRPC, nil
}
// IsConnError 是否为连接错误
func IsConnError(err error) bool {
if err == nil {
return false
}
// 检查是否为连接错误
statusErr, ok := status.FromError(err)
if ok {
var errorCode = statusErr.Code()
return errorCode == codes.Unavailable || errorCode == codes.Canceled
}
if strings.Contains(err.Error(), "code = Canceled") {
return true
}
return false
}
// IsUnimplementedError 检查是否为未实现错误
func IsUnimplementedError(err error) bool {
if err == nil {
return false
}
statusErr, ok := status.FromError(err)
if ok {
if statusErr.Code() == codes.Unimplemented {
return true
}
}
return false
}

View File

@@ -0,0 +1,23 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
)
var isConfigured bool
// IsConfigured 判断系统是否已经配置过
func IsConfigured() bool {
if isConfigured {
return true
}
_, err := configs.LoadAPIConfig()
isConfigured = err == nil
return isConfigured
}
// IsNewInstalled IsNew 检查是否新安装
func IsNewInstalled() bool {
return configs.IsNewInstalled()
}

View File

@@ -0,0 +1,177 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/plus"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
plusutils "github.com/TeaOSLab/EdgePlus/pkg/utils"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/logs"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
func init() {
events.On(events.EventStart, func() {
var task = NewAuthorityTask()
goman.New(func() {
task.Start()
})
})
}
var authorityTaskNotifyChange = make(chan bool, 1)
func NotifyAuthorityTask() {
select {
case authorityTaskNotifyChange <- true:
default:
}
}
type AuthorityTask struct {
oldIsPlus bool
}
func NewAuthorityTask() *AuthorityTask {
return &AuthorityTask{}
}
func (this *AuthorityTask) Start() {
// 从缓存中读取
var config = configs.ReadPlusConfig()
if config != nil {
teaconst.IsPlus = config.IsPlus
this.notifyChange()
plus.UpdateComponents(config.Edition, config.Components)
}
// 开始计时器
var ticker = time.NewTicker(10 * time.Minute)
if Tea.IsTesting() {
// 快速测试
ticker = time.NewTicker(1 * time.Minute)
}
// 初始化的时候先获取一次
var timeout = time.NewTimer(3 * time.Second)
<-timeout.C
err := this.Loop()
if err != nil {
logs.Println("[TASK][AuthorityTask]" + err.Error())
}
// 定时获取
for {
select {
case <-ticker.C:
case <-authorityTaskNotifyChange:
}
err := this.Loop()
if err != nil {
logs.Println("[TASK][AuthorityTask]" + err.Error())
}
}
}
func (this *AuthorityTask) Loop() error {
// 如果还没有安装直接返回
if !setup.IsConfigured() {
return nil
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
resp, err := rpcClient.AuthorityKeyRPC().ReadAuthorityKey(rpcClient.Context(0), &pb.ReadAuthorityKeyRequest{})
if err != nil {
teaconst.IsPlus = false
if !rpc.IsConnError(err) {
this.notifyChange()
}
return err
}
defer this.notifyChange()
if resp.AuthorityKey == nil {
teaconst.IsPlus = false
return nil
}
plus.ErrString = ""
var plusEdition = ""
if resp.AuthorityKey != nil && len(resp.AuthorityKey.Value) > 0 && resp.AuthorityKey.DayTo >= timeutil.Format("Y-m-d") {
nativeKey, err := plusutils.DecodeKey([]byte(resp.AuthorityKey.Value))
if err != nil {
teaconst.IsPlus = false
return nil
}
var isOk = true
if len(nativeKey.RequestCode) > 0 {
isOk, _ = plusutils.ValidateRequestCode(nativeKey.RequestCode)
} else if len(nativeKey.MacAddresses) > 0 {
var macAddresses = resp.AuthorityKey.MacAddresses
for _, addr := range macAddresses {
if !plus.ValidateMac(addr) {
isOk = false
break
}
}
}
if isOk {
plusEdition = resp.AuthorityKey.Edition
teaconst.IsPlus = true
plus.UpdateComponents(resp.AuthorityKey.Edition, resp.AuthorityKey.Components)
} else {
plus.ErrString = "当前管理平台所在服务器环境与商业版认证分发时发生了变化,因此无法使用商业版功能。如果你正在迁移管理平台,请联系开发者重新申请新的注册码。"
teaconst.IsPlus = false
}
} else {
teaconst.IsPlus = false
}
_ = configs.WritePlusConfig(&configs.PlusConfig{
Edition: plusEdition,
IsPlus: teaconst.IsPlus,
Components: resp.AuthorityKey.Components,
DayTo: resp.AuthorityKey.DayTo,
})
return nil
}
func (this *AuthorityTask) notifyChange() {
if this.oldIsPlus == teaconst.IsPlus {
return
}
this.oldIsPlus = teaconst.IsPlus
go func() {
time.Sleep(1 * time.Second) // 等待默认的加载程序完成
var err error
if teaconst.IsPlus {
err = iplibrary.InitPlus()
} else {
err = iplibrary.InitDefault()
}
if err != nil {
logs.Println("[NODE]initialize ip library failed: " + err.Error())
}
}()
}

View File

@@ -0,0 +1,133 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package tasks
import (
"encoding/json"
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
"io"
"net/http"
"runtime"
"strings"
"time"
)
func init() {
events.On(events.EventStart, func() {
var task = NewCheckUpdatesTask()
goman.New(func() {
task.Start()
})
})
}
type CheckUpdatesTask struct {
ticker *time.Ticker
}
func NewCheckUpdatesTask() *CheckUpdatesTask {
return &CheckUpdatesTask{}
}
func (this *CheckUpdatesTask) Start() {
this.ticker = time.NewTicker(12 * time.Hour)
for range this.ticker.C {
err := this.Loop()
if err != nil {
logs.Println("[TASK][CHECK_UPDATES_TASK]" + err.Error())
}
}
}
func (this *CheckUpdatesTask) Loop() error {
// 检查是否开启
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
valueResp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeCheckUpdates})
if err != nil {
return err
}
var valueJSON = valueResp.ValueJSON
var config = systemconfigs.NewCheckUpdatesConfig()
if len(valueJSON) > 0 {
err = json.Unmarshal(valueJSON, config)
if err != nil {
return fmt.Errorf("decode config failed: %w", err)
}
if !config.AutoCheck {
return nil
}
} else {
return nil
}
// 开始检查
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// 目前支持Linux
if runtime.GOOS != "linux" {
return nil
}
var apiURL = teaconst.UpdatesURL
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
resp, err := http.Get(apiURL)
if err != nil {
return fmt.Errorf("read api failed: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read api failed: %w", err)
}
var apiResponse = &Response{}
err = json.Unmarshal(data, apiResponse)
if err != nil {
return fmt.Errorf("decode version data failed: %w", err)
}
if apiResponse.Code != 200 {
return errors.New("invalid response: " + apiResponse.Message)
}
var m = maps.NewMap(apiResponse.Data)
var dlHost = m.GetString("host")
var versions = m.GetSlice("versions")
if len(versions) > 0 {
for _, version := range versions {
var vMap = maps.NewMap(version)
if vMap.GetString("code") == "admin" {
var latestVersion = vMap.GetString("version")
if stringutil.VersionCompare(teaconst.Version, latestVersion) < 0 && (len(config.IgnoredVersion) == 0 || stringutil.VersionCompare(latestVersion, config.IgnoredVersion) > 0) {
teaconst.NewVersionCode = latestVersion
teaconst.NewVersionDownloadURL = dlHost + vMap.GetString("url")
return nil
}
}
}
}
return nil
}

View File

@@ -0,0 +1,166 @@
package tasks
import (
"context"
"crypto/tls"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/logs"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"net/url"
"sort"
"strings"
"sync"
"time"
)
func init() {
events.On(events.EventStart, func() {
task := NewSyncAPINodesTask()
goman.New(func() {
task.Start()
})
})
}
// SyncAPINodesTask API节点同步任务
type SyncAPINodesTask struct {
}
func NewSyncAPINodesTask() *SyncAPINodesTask {
return &SyncAPINodesTask{}
}
func (this *SyncAPINodesTask) Start() {
ticker := time.NewTicker(5 * time.Minute)
if Tea.IsTesting() {
// 快速测试
ticker = time.NewTicker(1 * time.Minute)
}
for range ticker.C {
err := this.Loop()
if err != nil {
logs.Println("[TASK][SYNC_API_NODES]" + err.Error())
}
}
}
func (this *SyncAPINodesTask) Loop() error {
// 如果还没有安装直接返回
if !setup.IsConfigured() || teaconst.IsRecoverMode {
return nil
}
config, err := configs.LoadAPIConfig()
if err != nil {
return err
}
// 是否禁止自动升级
if config.RPCDisableUpdate {
return nil
}
// 获取所有可用的节点
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
resp, err := rpcClient.APINodeRPC().FindAllEnabledAPINodes(rpcClient.Context(0), &pb.FindAllEnabledAPINodesRequest{})
if err != nil {
return err
}
var newEndpoints = []string{}
for _, node := range resp.ApiNodes {
if !node.IsOn {
continue
}
newEndpoints = append(newEndpoints, node.AccessAddrs...)
}
// 和现有的对比
if this.isSame(newEndpoints, config.RPCEndpoints) {
return nil
}
// 测试是否有API节点可用
hasOk := this.testEndpoints(newEndpoints)
if !hasOk {
return nil
}
// 修改RPC对象配置
config.RPCEndpoints = newEndpoints
err = rpcClient.UpdateConfig(config)
if err != nil {
return err
}
// 保存到文件
err = config.WriteFile(Tea.ConfigFile(configs.ConfigFileName))
if err != nil {
return err
}
return nil
}
func (this *SyncAPINodesTask) isSame(endpoints1 []string, endpoints2 []string) bool {
sort.Strings(endpoints1)
sort.Strings(endpoints2)
return strings.Join(endpoints1, "&") == strings.Join(endpoints2, "&")
}
func (this *SyncAPINodesTask) testEndpoints(endpoints []string) bool {
if len(endpoints) == 0 {
return false
}
var wg = sync.WaitGroup{}
wg.Add(len(endpoints))
var ok = false
for _, endpoint := range endpoints {
go func(endpoint string) {
defer wg.Done()
u, err := url.Parse(endpoint)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer func() {
cancel()
}()
var conn *grpc.ClientConn
if u.Scheme == "http" {
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
} else if u.Scheme == "https" {
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
InsecureSkipVerify: true,
})), grpc.WithBlock())
}
if err != nil {
return
}
_ = conn.Close()
ok = true
}(endpoint)
}
wg.Wait()
return ok
}

View File

@@ -0,0 +1,15 @@
package tasks
import (
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
)
func TestSyncAPINodesTask_Loop(t *testing.T) {
task := NewSyncAPINodesTask()
err := task.Loop()
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,84 @@
package tasks
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/nodeutils"
"github.com/TeaOSLab/EdgeCommon/pkg/messageconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"time"
)
func init() {
events.On(events.EventStart, func() {
task := NewSyncClusterTask()
goman.New(func() {
task.Start()
})
})
}
// SyncClusterTask 自动同步集群任务
type SyncClusterTask struct {
}
func NewSyncClusterTask() *SyncClusterTask {
return &SyncClusterTask{}
}
func (this *SyncClusterTask) Start() {
ticker := time.NewTicker(3 * time.Second)
for range ticker.C {
err := this.loop()
if err != nil {
logs.Println("[TASK][SYNC_CLUSTER]" + err.Error())
}
}
}
func (this *SyncClusterTask) loop() error {
// 如果还没有安装直接返回
if !setup.IsConfigured() || teaconst.IsRecoverMode {
return nil
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
ctx := rpcClient.Context(0)
tasksResp, err := rpcClient.NodeTaskRPC().FindNotifyingNodeTasks(ctx, &pb.FindNotifyingNodeTasksRequest{Size: 500})
if err != nil {
return err
}
nodeIds := []int64{}
taskIds := []int64{}
for _, task := range tasksResp.NodeTasks {
if !lists.ContainsInt64(nodeIds, task.Node.Id) {
nodeIds = append(nodeIds, task.Node.Id)
}
taskIds = append(taskIds, task.Id)
}
if len(nodeIds) == 0 {
return nil
}
_, err = nodeutils.SendMessageToNodeIds(ctx, nodeIds, messageconfigs.MessageCodeNewNodeTask, &messageconfigs.NewNodeTaskMessage{}, 3)
if err != nil {
return err
}
// 设置已通知
_, err = rpcClient.NodeTaskRPC().UpdateNodeTasksNotified(ctx, &pb.UpdateNodeTasksNotifiedRequest{NodeTaskIds: taskIds})
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,12 @@
package tasks
import "testing"
func TestSyncClusterTask_loop(t *testing.T) {
task := NewSyncClusterTask()
err := task.loop()
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,145 @@
package ttlcache
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"time"
)
var DefaultCache = NewCache()
// TTL缓存
// 最大的缓存时间为30 * 86400
// Piece数据结构
//
// Piece1 | Piece2 | Piece3 | ...
// [ Item1, Item2, ... | ...
//
// KeyMap列表数据结构
// { timestamp1 => [key1, key2, ...] }, ...
type Cache struct {
isDestroyed bool
pieces []*Piece
countPieces uint64
maxItems int
gcPieceIndex int
ticker *utils.Ticker
}
func NewCache(opt ...OptionInterface) *Cache {
countPieces := 128
maxItems := 1_000_000
for _, option := range opt {
if option == nil {
continue
}
switch o := option.(type) {
case *PiecesOption:
if o.Count > 0 {
countPieces = o.Count
}
case *MaxItemsOption:
if o.Count > 0 {
maxItems = o.Count
}
}
}
cache := &Cache{
countPieces: uint64(countPieces),
maxItems: maxItems,
}
for i := 0; i < countPieces; i++ {
cache.pieces = append(cache.pieces, NewPiece(maxItems/countPieces))
}
// start timer
go func() {
cache.ticker = utils.NewTicker(5 * time.Second)
for cache.ticker.Next() {
cache.GC()
}
}()
return cache
}
func (this *Cache) Write(key string, value interface{}, expiredAt int64) {
if this.isDestroyed {
return
}
currentTimestamp := time.Now().Unix()
if expiredAt <= currentTimestamp {
return
}
maxExpiredAt := currentTimestamp + 30*86400
if expiredAt > maxExpiredAt {
expiredAt = maxExpiredAt
}
uint64Key := HashKey([]byte(key))
pieceIndex := uint64Key % this.countPieces
this.pieces[pieceIndex].Add(uint64Key, &Item{
Value: value,
expiredAt: expiredAt,
})
}
func (this *Cache) IncreaseInt64(key string, delta int64, expiredAt int64) int64 {
if this.isDestroyed {
return 0
}
currentTimestamp := time.Now().Unix()
if expiredAt <= currentTimestamp {
return 0
}
maxExpiredAt := currentTimestamp + 30*86400
if expiredAt > maxExpiredAt {
expiredAt = maxExpiredAt
}
uint64Key := HashKey([]byte(key))
pieceIndex := uint64Key % this.countPieces
return this.pieces[pieceIndex].IncreaseInt64(uint64Key, delta, expiredAt)
}
func (this *Cache) Read(key string) (item *Item) {
uint64Key := HashKey([]byte(key))
return this.pieces[uint64Key%this.countPieces].Read(uint64Key)
}
func (this *Cache) Delete(key string) {
uint64Key := HashKey([]byte(key))
this.pieces[uint64Key%this.countPieces].Delete(uint64Key)
}
func (this *Cache) Count() (count int) {
for _, piece := range this.pieces {
count += piece.Count()
}
return
}
func (this *Cache) GC() {
this.pieces[this.gcPieceIndex].GC()
newIndex := this.gcPieceIndex + 1
if newIndex >= int(this.countPieces) {
newIndex = 0
}
this.gcPieceIndex = newIndex
}
func (this *Cache) Destroy() {
this.isDestroyed = true
if this.ticker != nil {
this.ticker.Stop()
this.ticker = nil
}
for _, piece := range this.pieces {
piece.Destroy()
}
}

View File

@@ -0,0 +1,124 @@
package ttlcache
import (
"github.com/iwind/TeaGo/rands"
"runtime"
"strconv"
"testing"
"time"
)
func TestNewCache(t *testing.T) {
cache := NewCache()
cache.Write("a", 1, time.Now().Unix()+3600)
cache.Write("b", 2, time.Now().Unix()+3601)
cache.Write("a", 1, time.Now().Unix()+3602)
cache.Write("d", 1, time.Now().Unix()+1)
for _, piece := range cache.pieces {
if len(piece.m) > 0 {
for k, item := range piece.m {
t.Log(k, "=>", item.Value, item.expiredAt)
}
}
}
t.Log(cache.Read("a"))
time.Sleep(2 * time.Second)
t.Log(cache.Read("d"))
}
func BenchmarkCache_Add(b *testing.B) {
runtime.GOMAXPROCS(1)
cache := NewCache()
for i := 0; i < b.N; i++ {
cache.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(i%1024))
}
}
func TestCache_IncreaseInt64(t *testing.T) {
var cache = NewCache()
{
cache.IncreaseInt64("a", 1, time.Now().Unix()+3600)
t.Log(cache.Read("a"))
}
{
cache.IncreaseInt64("a", 1, time.Now().Unix()+3600+1)
t.Log(cache.Read("a"))
}
{
cache.Write("b", 1, time.Now().Unix()+3600+2)
t.Log(cache.Read("b"))
}
{
cache.IncreaseInt64("b", 1, time.Now().Unix()+3600+3)
t.Log(cache.Read("b"))
}
}
func TestCache_Read(t *testing.T) {
runtime.GOMAXPROCS(1)
var cache = NewCache(PiecesOption{Count: 32})
for i := 0; i < 10_000_000; i++ {
cache.Write("HELLO_WORLD_"+strconv.Itoa(i), i, time.Now().Unix()+int64(i%10240)+1)
}
total := 0
for _, piece := range cache.pieces {
//t.Log(len(piece.m), "keys")
total += len(piece.m)
}
t.Log(total, "total keys")
before := time.Now()
for i := 0; i < 10_240; i++ {
_ = cache.Read("HELLO_WORLD_" + strconv.Itoa(i))
}
t.Log(time.Since(before).Seconds()*1000, "ms")
}
func TestCache_GC(t *testing.T) {
var cache = NewCache(&PiecesOption{Count: 5})
cache.Write("a", 1, time.Now().Unix()+1)
cache.Write("b", 2, time.Now().Unix()+2)
cache.Write("c", 3, time.Now().Unix()+3)
cache.Write("d", 4, time.Now().Unix()+4)
cache.Write("e", 5, time.Now().Unix()+10)
go func() {
for i := 0; i < 1000; i++ {
cache.Write("f", 1, time.Now().Unix()+1)
time.Sleep(10 * time.Millisecond)
}
}()
for i := 0; i < 20; i++ {
cache.GC()
t.Log("items:", cache.Count())
time.Sleep(1 * time.Second)
}
t.Log("now:", time.Now().Unix())
for _, p := range cache.pieces {
for k, v := range p.m {
t.Log(k, v.Value, v.expiredAt)
}
}
}
func TestCache_GC2(t *testing.T) {
runtime.GOMAXPROCS(1)
cache := NewCache()
for i := 0; i < 1_000_000; i++ {
cache.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(0, 100)))
}
for i := 0; i < 100; i++ {
t.Log(cache.Count(), "items")
time.Sleep(1 * time.Second)
}
}

View File

@@ -0,0 +1,6 @@
package ttlcache
type Item struct {
Value interface{}
expiredAt int64
}

View File

@@ -0,0 +1,20 @@
package ttlcache
type OptionInterface interface {
}
type PiecesOption struct {
Count int
}
func NewPiecesOption(count int) *PiecesOption {
return &PiecesOption{Count: count}
}
type MaxItemsOption struct {
Count int
}
func NewMaxItemsOption(count int) *MaxItemsOption {
return &MaxItemsOption{Count: count}
}

View File

@@ -0,0 +1,88 @@
package ttlcache
import (
"github.com/iwind/TeaGo/types"
"sync"
"time"
)
type Piece struct {
m map[uint64]*Item
maxItems int
locker sync.RWMutex
}
func NewPiece(maxItems int) *Piece {
return &Piece{m: map[uint64]*Item{}, maxItems: maxItems}
}
func (this *Piece) Add(key uint64, item *Item) {
this.locker.Lock()
if len(this.m) >= this.maxItems {
this.locker.Unlock()
return
}
this.m[key] = item
this.locker.Unlock()
}
func (this *Piece) IncreaseInt64(key uint64, delta int64, expiredAt int64) (result int64) {
this.locker.Lock()
item, ok := this.m[key]
if ok {
result = types.Int64(item.Value) + delta
item.Value = result
item.expiredAt = expiredAt
} else {
if len(this.m) < this.maxItems {
result = delta
this.m[key] = &Item{
Value: delta,
expiredAt: expiredAt,
}
}
}
this.locker.Unlock()
return
}
func (this *Piece) Delete(key uint64) {
this.locker.Lock()
delete(this.m, key)
this.locker.Unlock()
}
func (this *Piece) Read(key uint64) (item *Item) {
this.locker.RLock()
item = this.m[key]
if item != nil && item.expiredAt < time.Now().Unix() {
item = nil
}
this.locker.RUnlock()
return
}
func (this *Piece) Count() (count int) {
this.locker.RLock()
count = len(this.m)
this.locker.RUnlock()
return
}
func (this *Piece) GC() {
this.locker.Lock()
timestamp := time.Now().Unix()
for k, item := range this.m {
if item.expiredAt <= timestamp {
delete(this.m, k)
}
}
this.locker.Unlock()
}
func (this *Piece) Destroy() {
this.locker.Lock()
this.m = nil
this.locker.Unlock()
}

View File

@@ -0,0 +1,60 @@
package ttlcache
import (
"github.com/iwind/TeaGo/rands"
"testing"
"time"
)
func TestPiece_Add(t *testing.T) {
piece := NewPiece(10)
piece.Add(1, &Item{expiredAt: time.Now().Unix() + 3600})
piece.Add(2, &Item{})
piece.Add(3, &Item{})
piece.Delete(3)
for key, item := range piece.m {
t.Log(key, item.Value)
}
t.Log(piece.Read(1))
}
func TestPiece_MaxItems(t *testing.T) {
piece := NewPiece(10)
for i := 0; i < 1000; i++ {
piece.Add(uint64(i), &Item{expiredAt: time.Now().Unix() + 3600})
}
t.Log(len(piece.m))
}
func TestPiece_GC(t *testing.T) {
piece := NewPiece(10)
piece.Add(1, &Item{Value: 1, expiredAt: time.Now().Unix() + 1})
piece.Add(2, &Item{Value: 2, expiredAt: time.Now().Unix() + 1})
piece.Add(3, &Item{Value: 3, expiredAt: time.Now().Unix() + 1})
t.Log("before gc ===")
for key, item := range piece.m {
t.Log(key, item.Value)
}
time.Sleep(1 * time.Second)
piece.GC()
t.Log("after gc ===")
for key, item := range piece.m {
t.Log(key, item.Value)
}
}
func TestPiece_GC2(t *testing.T) {
piece := NewPiece(10)
for i := 0; i < 10_000; i++ {
piece.Add(uint64(i), &Item{Value: 1, expiredAt: time.Now().Unix() + int64(rands.Int(1, 10))})
}
time.Sleep(1 * time.Second)
before := time.Now()
piece.GC()
t.Log(time.Since(before).Seconds()*1000, "ms")
t.Log(piece.Count())
}

View File

@@ -0,0 +1,7 @@
package ttlcache
import "github.com/cespare/xxhash/v2"
func HashKey(key []byte) uint64 {
return xxhash.Sum64(key)
}

View File

@@ -0,0 +1,13 @@
package ttlcache
import (
"runtime"
"testing"
)
func BenchmarkHashKey(b *testing.B) {
runtime.GOMAXPROCS(1)
for i := 0; i < b.N; i++ {
HashKey([]byte("HELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLD"))
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package apinodeutils
import (
"crypto/md5"
"fmt"
"io"
"os"
)
// DeployFile 部署文件描述
type DeployFile struct {
OS string
Arch string
Version string
Path string
}
// Sum 计算概要
func (this *DeployFile) Sum() (string, error) {
fp, err := os.Open(this.Path)
if err != nil {
return "", err
}
defer func() {
_ = fp.Close()
}()
m := md5.New()
buffer := make([]byte, 128*1024)
for {
n, err := fp.Read(buffer)
if err != nil {
if err == io.EOF {
break
}
return "", err
}
_, err = m.Write(buffer[:n])
if err != nil {
return "", err
}
}
sum := m.Sum(nil)
return fmt.Sprintf("%x", sum), nil
}
// Read 读取一个片段数据
func (this *DeployFile) Read(offset int64) (data []byte, newOffset int64, err error) {
fp, err := os.Open(this.Path)
if err != nil {
return nil, offset, err
}
defer func() {
_ = fp.Close()
}()
stat, err := fp.Stat()
if err != nil {
return nil, offset, err
}
if offset >= stat.Size() {
return nil, offset, io.EOF
}
_, err = fp.Seek(offset, io.SeekStart)
if err != nil {
return nil, offset, err
}
buffer := make([]byte, 128*1024)
n, err := fp.Read(buffer)
if err != nil {
return nil, offset, err
}
return buffer[:n], offset + int64(n), nil
}

View File

@@ -0,0 +1,96 @@
package apinodeutils
import (
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
stringutil "github.com/iwind/TeaGo/utils/string"
"regexp"
)
// DeployManager 节点部署文件管理器
// 如果节点部署文件有变化需要重启API节点以便于生效
type DeployManager struct {
dir string
}
// NewDeployManager 获取新节点部署文件管理器
func NewDeployManager() *DeployManager {
var manager = &DeployManager{
dir: Tea.Root + "/edge-api/deploy",
}
manager.LoadNodeFiles()
manager.LoadNSNodeFiles()
return manager
}
// LoadNodeFiles 加载所有边缘节点文件
func (this *DeployManager) LoadNodeFiles() []*DeployFile {
var keyMap = map[string]*DeployFile{} // key => File
var reg = regexp.MustCompile(`^edge-node-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
for _, file := range files.NewFile(this.dir).List() {
var name = file.Name()
if !reg.MatchString(name) {
continue
}
var matches = reg.FindStringSubmatch(name)
var osName = matches[1]
var arch = matches[2]
var version = matches[3]
var key = osName + "_" + arch
oldFile, ok := keyMap[key]
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
continue
}
keyMap[key] = &DeployFile{
OS: osName,
Arch: arch,
Version: version,
Path: file.Path(),
}
}
var result = []*DeployFile{}
for _, v := range keyMap {
result = append(result, v)
}
return result
}
// LoadNSNodeFiles 加载所有NS节点安装文件
func (this *DeployManager) LoadNSNodeFiles() []*DeployFile {
var keyMap = map[string]*DeployFile{} // key => File
var reg = regexp.MustCompile(`^edge-dns-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
for _, file := range files.NewFile(this.dir).List() {
var name = file.Name()
if !reg.MatchString(name) {
continue
}
var matches = reg.FindStringSubmatch(name)
var osName = matches[1]
var arch = matches[2]
var version = matches[3]
var key = osName + "_" + arch
oldFile, ok := keyMap[key]
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
continue
}
keyMap[key] = &DeployFile{
OS: osName,
Arch: arch,
Version: version,
Path: file.Path(),
}
}
var result = []*DeployFile{}
for _, v := range keyMap {
result = append(result, v)
}
return result
}

View File

@@ -0,0 +1,30 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
var SharedManager = NewManager()
type Manager struct {
upgraderMap map[int64]*Upgrader
}
func NewManager() *Manager {
return &Manager{
upgraderMap: map[int64]*Upgrader{},
}
}
func (this *Manager) AddUpgrader(upgrader *Upgrader) {
this.upgraderMap[upgrader.apiNodeId] = upgrader
}
func (this *Manager) FindUpgrader(apiNodeId int64) *Upgrader {
return this.upgraderMap[apiNodeId]
}
func (this *Manager) RemoveUpgrader(upgrader *Upgrader) {
if upgrader == nil {
return
}
delete(this.upgraderMap, upgrader.apiNodeId)
}

View File

@@ -0,0 +1,349 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
import (
"compress/gzip"
"context"
"crypto/md5"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"io"
"os"
"path/filepath"
"runtime"
)
type Progress struct {
Percent float64
}
type Upgrader struct {
progress *Progress
apiExe string
apiNodeId int64
}
func NewUpgrader(apiNodeId int64) *Upgrader {
return &Upgrader{
apiExe: apiExe(),
progress: &Progress{Percent: 0},
apiNodeId: apiNodeId,
}
}
func (this *Upgrader) Upgrade() error {
sharedClient, err := rpc.SharedRPC()
if err != nil {
return err
}
apiNodeResp, err := sharedClient.APINodeRPC().FindEnabledAPINode(sharedClient.Context(0), &pb.FindEnabledAPINodeRequest{ApiNodeId: this.apiNodeId})
if err != nil {
return err
}
var apiNode = apiNodeResp.ApiNode
if apiNode == nil {
return errors.New("could not find api node with id '" + types.String(this.apiNodeId) + "'")
}
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
return err
}
var newAPIConfig = apiConfig.Clone()
newAPIConfig.RPCEndpoints = apiNode.AccessAddrs
rpcClient, err := rpc.NewRPCClient(newAPIConfig, false)
if err != nil {
return err
}
// 升级边缘节点
err = this.upgradeNodes(sharedClient.Context(0), rpcClient)
if err != nil {
return err
}
// 升级NS节点
err = this.upgradeNSNodes(sharedClient.Context(0), rpcClient)
if err != nil {
return err
}
// 升级API节点
err = this.upgradeAPINode(sharedClient.Context(0), rpcClient)
if err != nil {
return fmt.Errorf("upgrade api node failed: %w", err)
}
return nil
}
// Progress 查看升级进程
func (this *Upgrader) Progress() *Progress {
return this.progress
}
// 升级API节点
func (this *Upgrader) upgradeAPINode(ctx context.Context, rpcClient *rpc.RPCClient) error {
versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(ctx, &pb.FindCurrentAPINodeVersionRequest{})
if err != nil {
return err
}
if !Tea.IsTesting() /** 开发环境下允许突破此限制方便测试 **/ &&
(stringutil.VersionCompare(versionResp.Version, "0.6.4" /** 从0.6.4开始支持 **/) < 0 || versionResp.Os != runtime.GOOS || versionResp.Arch != runtime.GOARCH) {
return errors.New("could not upgrade api node v" + versionResp.Version + "/" + versionResp.Os + "/" + versionResp.Arch)
}
// 检查本地文件版本
canUpgrade, reason := CanUpgrade(versionResp.Version, versionResp.Os, versionResp.Arch)
if !canUpgrade {
return errors.New(reason)
}
localVersion, err := lookupLocalVersion()
if err != nil {
return fmt.Errorf("lookup version failed: %w", err)
}
// 检查要升级的文件
var gzFile = this.apiExe + "." + localVersion + ".gz"
gzReader, err := os.Open(gzFile)
if err != nil {
if !os.IsNotExist(err) {
return err
}
err = func() error {
// 压缩文件
exeReader, err := os.Open(this.apiExe)
if err != nil {
return err
}
defer func() {
_ = exeReader.Close()
}()
var tmpGzFile = gzFile + ".tmp"
gzFileWriter, err := os.OpenFile(tmpGzFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
return err
}
var gzWriter = gzip.NewWriter(gzFileWriter)
defer func() {
_ = gzWriter.Close()
_ = gzFileWriter.Close()
_ = os.Rename(tmpGzFile, gzFile)
}()
_, err = io.Copy(gzWriter, exeReader)
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
gzReader, err = os.Open(gzFile)
if err != nil {
return err
}
}
defer func() {
_ = gzReader.Close()
}()
// 开始上传
var hash = md5.New()
var buf = make([]byte, 128*4096)
var isFirst = true
stat, err := gzReader.Stat()
if err != nil {
return err
}
var totalSize = stat.Size()
if totalSize == 0 {
_ = gzReader.Close()
_ = os.Remove(gzFile)
return errors.New("invalid gz file")
}
var uploadedSize int64 = 0
for {
n, err := gzReader.Read(buf)
if n > 0 {
// 计算Hash
hash.Write(buf[:n])
// 上传
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
Filename: filepath.Base(this.apiExe),
Sum: "",
ChunkData: buf[:n],
IsFirstChunk: isFirst,
IsLastChunk: false,
})
if uploadErr != nil {
return uploadErr
}
// 进度
uploadedSize += int64(n)
this.progress = &Progress{Percent: float64(uploadedSize*100) / float64(totalSize)}
}
if isFirst {
isFirst = false
}
if err != nil {
if err != io.EOF {
return err
}
if err == io.EOF {
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
Filename: filepath.Base(this.apiExe),
Sum: fmt.Sprintf("%x", hash.Sum(nil)),
ChunkData: buf[:n],
IsFirstChunk: isFirst,
IsLastChunk: true,
})
if uploadErr != nil {
return uploadErr
}
break
}
}
}
return nil
}
// 升级边缘节点
func (this *Upgrader) upgradeNodes(ctx context.Context, rpcClient *rpc.RPCClient) error {
// 本地的
var manager = NewDeployManager()
var localFileMap = map[string]*DeployFile{} // os_arch => *DeployFile
for _, deployFile := range manager.LoadNodeFiles() {
localFileMap[deployFile.OS+"_"+deployFile.Arch] = deployFile
}
remoteFilesResp, err := rpcClient.APINodeRPC().FindLatestDeployFiles(ctx, &pb.FindLatestDeployFilesRequest{})
if err != nil {
return err
}
var remoteFileMap = map[string]*pb.FindLatestDeployFilesResponse_DeployFile{} // os_arch => *DeployFile
for _, nodeFile := range remoteFilesResp.NodeDeployFiles {
remoteFileMap[nodeFile.Os+"_"+nodeFile.Arch] = nodeFile
}
// 对比版本
for key, deployFile := range localFileMap {
remoteDeployFile, ok := remoteFileMap[key]
if !ok || stringutil.VersionCompare(remoteDeployFile.Version, deployFile.Version) < 0 {
err = this.uploadNodeDeployFile(ctx, rpcClient, deployFile.Path)
if err != nil {
return fmt.Errorf("upload deploy file '%s' failed: %w", filepath.Base(deployFile.Path), err)
}
}
}
return nil
}
// 升级NS节点
func (this *Upgrader) upgradeNSNodes(ctx context.Context, rpcClient *rpc.RPCClient) error {
// 本地的
var manager = NewDeployManager()
var localFileMap = map[string]*DeployFile{} // os_arch => *DeployFile
for _, deployFile := range manager.LoadNSNodeFiles() {
localFileMap[deployFile.OS+"_"+deployFile.Arch] = deployFile
}
remoteFilesResp, err := rpcClient.APINodeRPC().FindLatestDeployFiles(ctx, &pb.FindLatestDeployFilesRequest{})
if err != nil {
return err
}
var remoteFileMap = map[string]*pb.FindLatestDeployFilesResponse_DeployFile{} // os_arch => *DeployFile
for _, nodeFile := range remoteFilesResp.NsNodeDeployFiles {
remoteFileMap[nodeFile.Os+"_"+nodeFile.Arch] = nodeFile
}
// 对比版本
for key, deployFile := range localFileMap {
remoteDeployFile, ok := remoteFileMap[key]
if !ok || stringutil.VersionCompare(remoteDeployFile.Version, deployFile.Version) < 0 {
err = this.uploadNodeDeployFile(ctx, rpcClient, deployFile.Path)
if err != nil {
return fmt.Errorf("upload deploy file '%s' failed: %w", filepath.Base(deployFile.Path), err)
}
}
}
return nil
}
// 上传节点文件
func (this *Upgrader) uploadNodeDeployFile(ctx context.Context, rpcClient *rpc.RPCClient, path string) error {
fp, err := os.Open(path)
if err != nil {
return err
}
defer func() {
_ = fp.Close()
}()
var buf = make([]byte, 128*4096)
var isFirst = true
var hash = md5.New()
for {
n, err := fp.Read(buf)
if n > 0 {
hash.Write(buf[:n])
_, uploadErr := rpcClient.APINodeRPC().UploadDeployFileToAPINode(ctx, &pb.UploadDeployFileToAPINodeRequest{
Filename: filepath.Base(path),
Sum: "",
ChunkData: buf[:n],
IsFirstChunk: isFirst,
IsLastChunk: false,
})
if uploadErr != nil {
return uploadErr
}
isFirst = false
}
if err != nil {
if err == io.EOF {
err = nil
_, uploadErr := rpcClient.APINodeRPC().UploadDeployFileToAPINode(ctx, &pb.UploadDeployFileToAPINodeRequest{
Filename: filepath.Base(path),
Sum: fmt.Sprintf("%x", hash.Sum(nil)),
ChunkData: nil,
IsFirstChunk: false,
IsLastChunk: true,
})
if uploadErr != nil {
return uploadErr
}
break
}
return err
}
}
return nil
}

View File

@@ -0,0 +1,22 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils_test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
_ "github.com/iwind/TeaGo/bootstrap"
"runtime"
"testing"
)
func TestUpgrader_CanUpgrade(t *testing.T) {
t.Log(apinodeutils.CanUpgrade("0.6.3", runtime.GOOS, runtime.GOARCH))
}
func TestUpgrader_Upgrade(t *testing.T) {
var upgrader = apinodeutils.NewUpgrader(1)
err := upgrader.Upgrade()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
import (
"bytes"
"errors"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/Tea"
stringutil "github.com/iwind/TeaGo/utils/string"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
)
func CanUpgrade(apiVersion string, osName string, arch string) (canUpgrade bool, reason string) {
if len(apiVersion) == 0 {
return false, "current api version should not be empty"
}
if stringutil.VersionCompare(apiVersion, "0.6.4") < 0 {
return false, "api node version must greater than or equal to 0.6.4"
}
if osName != runtime.GOOS {
return false, "os not match: " + osName
}
if arch != runtime.GOARCH {
return false, "arch not match: " + arch
}
stat, err := os.Stat(apiExe())
if err != nil {
return false, "stat error: " + err.Error()
}
if stat.IsDir() {
return false, "is directory"
}
localVersion, err := lookupLocalVersion()
if err != nil {
return false, "lookup version failed: " + err.Error()
}
if localVersion != teaconst.APINodeVersion {
return false, "not newest api node"
}
if stringutil.VersionCompare(localVersion, apiVersion) <= 0 {
return false, "need not upgrade, local '" + localVersion + "' vs remote '" + apiVersion + "'"
}
return true, ""
}
func lookupLocalVersion() (string, error) {
var cmd = exec.Command(apiExe(), "-V")
var output = &bytes.Buffer{}
cmd.Stdout = output
err := cmd.Run()
if err != nil {
return "", err
}
var localVersion = strings.TrimSpace(output.String())
// 检查版本号
var reg = regexp.MustCompile(`^[\d.]+$`)
if !reg.MatchString(localVersion) {
return "", errors.New("lookup version failed: " + localVersion)
}
return localVersion, nil
}
func apiExe() string {
return Tea.Root + "/edge-api/bin/edge-api"
}

View File

@@ -0,0 +1,12 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dateutils
// SplitYmd 分隔Ymd格式的日期
// Ymd => Y-m-d
func SplitYmd(day string) string {
if len(day) != 8 {
return day
}
return day[:4] + "-" + day[4:6] + "-" + day[6:]
}

View File

@@ -0,0 +1,12 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import "regexp"
var emailReg = regexp.MustCompile(`(?i)^[a-z\d]+([._+-]*[a-z\d]+)*@([a-z\d]+[a-z\d-]*[a-z\d]+\.)+[a-z\d]+$`)
// ValidateEmail 校验电子邮箱格式
func ValidateEmail(email string) bool {
return emailReg.MatchString(email)
}

View File

@@ -0,0 +1,22 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils_test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestValidateEmail(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsTrue(utils.ValidateEmail("aaaa@gmail.com"))
a.IsTrue(utils.ValidateEmail("a.b@gmail.com"))
a.IsTrue(utils.ValidateEmail("a.b.c.d@gmail.com"))
a.IsTrue(utils.ValidateEmail("aaaa@gmail.com.cn"))
a.IsTrue(utils.ValidateEmail("hello.world.123@gmail.123.com"))
a.IsTrue(utils.ValidateEmail("10000@qq.com"))
a.IsFalse(utils.ValidateEmail("aaaa.@gmail.com"))
a.IsFalse(utils.ValidateEmail("aaaa@gmail"))
a.IsFalse(utils.ValidateEmail("aaaa@123"))
}

View File

@@ -0,0 +1,8 @@
package utils
import "github.com/iwind/TeaGo/logs"
func PrintError(err error) {
// TODO 记录调用的文件名、行数
logs.Println("[ERROR]" + err.Error())
}

View File

@@ -0,0 +1,308 @@
/*
@Author: 1usir
@Description:
@File: excelize
@Version: 1.0.0
@Date: 2024/2/21 14:10
*/
package exce
import (
"bytes"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/xuri/excelize/v2"
"io"
"strings"
)
type Rule struct {
Name string `json:"name"` // 序号 - 规则集名字
Type string `json:"type"` // 攻击类型 - 规则分组名称
Regular string `json:"regular"` // 正则
Regulars []string `json:"regulars"` // 正则集合
Level string `json:"level"` // 威胁等级
Position []string `json:"position"` // 参数位置 - 参数
Description string `json:"description"` // 描述 - 备注
CVE string `json:"cve"` // cve 编号
Inbound bool `json:"inbound"` // 入站规则
Outbound bool `json:"outbound"` // 出站规则
IsAnd bool `json:"is_and"` // 多条件
}
func saveErr(sheet string, nf *excelize.File, cols []string, sheetIndexs map[string]int) {
index, ok := sheetIndexs[sheet]
if !ok {
nf.NewSheet(sheet)
// 设置单元格的值
nf.SetCellValue(sheet, "A1", "序号")
nf.SetCellValue(sheet, "B1", "攻击类型")
nf.SetCellValue(sheet, "C1", "关键词")
nf.SetCellValue(sheet, "D1", "正则")
nf.SetCellValue(sheet, "E1", "威胁等级")
nf.SetCellValue(sheet, "F1", "攻击语句")
nf.SetCellValue(sheet, "G1", "攻击语句解码后")
nf.SetCellValue(sheet, "H1", "参数位置")
nf.SetCellValue(sheet, "I1", "描述")
nf.SetCellValue(sheet, "J1", "CVE编号")
nf.SetCellValue(sheet, "K1", "备注")
nf.SetCellValue(sheet, "L1", "错误原因")
sheetIndexs[sheet] = 2
index = 2
}
for i, col := range cols {
nf.SetCellValue(sheet, fmt.Sprintf("%c%d", 'A'+i, index), col)
}
sheetIndexs[sheet]++
}
func ParseRules(r io.Reader) (*bytes.Buffer, []*Rule, error) {
nf := excelize.NewFile()
nf.DeleteSheet("Sheet1")
f, err := excelize.OpenReader(r)
if err != nil {
return nil, nil, err
}
res := make([]*Rule, 0)
sheets := f.GetSheetList()
sheetIndexs := map[string]int{}
for _, sheet := range sheets {
rows, err := f.GetRows(sheet)
if err != nil || len(rows) <= 1 {
return nil, nil, err
}
/*
1 2 3 4 5 6 7 8 9 10 11 12
序号攻击类型关键字正则威胁等级攻击语句攻击语句解码后参数位置描述CVE编号备注错误原因
*/
for _, row := range rows[1:] {
cols := make([]string, 12)
copy(cols, row)
if len(cols) < 8 || cols[0] == "" || cols[1] == "" || cols[3] == "" || cols[7] == "" {
continue
}
r := &Rule{
Name: strings.TrimSpace(cols[0]),
Type: strings.TrimSpace(cols[1]),
Regular: strings.TrimSpace(cols[3]),
Level: strings.TrimSpace(cols[4]),
Position: strings.Split(cols[7], "\n"),
}
if strings.Contains(r.Regular, "\n") {
//fmt.Println(fmt.Sprintf("无效规则1Sheet[%s|%s] %s", sheet, r.Name, r.Regular))
//return nil, errors.New(fmt.Sprintf("无效规则Sheet[%s|%s] %s", sheet, r.Name, r.Regular))
// 创建错误新表格
cols[11] = "无效正则"
saveErr(sheet, nf, cols, sheetIndexs)
continue
}
if len(cols) > 8 {
r.Description = cols[8]
}
if len(cols) > 9 {
r.CVE = cols[9]
}
// 特殊处理
if r.Type == "xss注入" {
r.Type = "XSS"
}
// 支持多条件
var regulars []string
var positions []string
if strings.Contains(r.Regular, "且") {
regulars, positions, err = parseRegulars(r.Regular)
if err != nil {
//fmt.Println(fmt.Sprintf("多规则解析失败Sheet[%s|%s] %s %s", sheet, r.Name, r.Regular, err))
cols[11] = "多规则解析失败"
saveErr(sheet, nf, cols, sheetIndexs)
continue
}
r.IsAnd = true
} else {
regulars = []string{r.Regular}
}
for _, regular := range regulars {
// 校验正则参数是否合理
rule := &firewallconfigs.HTTPFirewallRule{
IsOn: true,
Operator: "match",
Value: regular,
IsCaseInsensitive: true,
}
if err := rule.Init(); err != nil {
//fmt.Println(fmt.Sprintf("无效正则规则Sheet[%s|%s] %s", sheet, r.Name, r.Regular))
// 创建错误新表格
cols[11] = "正则解析失败"
saveErr(sheet, nf, cols, sheetIndexs)
continue
}
}
if r.IsAnd {
r.Regulars = regulars
r.Position = r.setString(positions)
} else {
// position 格式化去重
r.Position = r.setString(r.Position)
}
res = append(res, r)
}
}
//nf.SaveAs("/Users/1usir/works/waf/open-waf/waf/EdgeAdmin/internal/utils/exce/WAF ALL Error.xlsx")
if len(sheetIndexs) > 0 {
_ = nf.DeleteSheet("Sheet1")
buff, err := nf.WriteToBuffer()
return buff, res, err
} else {
return nil, res, nil
}
}
func (this *Rule) formatPosition(p string) string {
switch p {
case "REQUEST_FILENAME", "REQUEST_FILENAM":
this.Inbound = true
return "${requestUpload.name}"
case "ARGS_NAMES", "ARGS":
this.Inbound = true
return "${args}"
case "REQUEST_BODY", "body":
this.Inbound = true
return "${requestBody}"
case "REQUEST_HEADERS", "head", "header", "headers":
this.Inbound = true
return "${headers}"
case "REQUEST_HEADERS_NAMES":
this.Inbound = true
return "${headerNames}"
case "REQUEST_COOKIES_NAMES", "REQUEST_COOKIES", "cookie":
this.Inbound = true
return "${cookies}"
case "url", "uri", "REQUEST_RAW_URI", "REQUEST_URI":
this.Inbound = true
return "${requestURI}"
case "RESPONSE_BODY":
this.Outbound = true
return "${responseBody}"
case "CONTENT_TYPE":
return "${contentType}"
case "referer":
return "${referer}"
case "host":
return "${host}"
default:
if strings.HasPrefix(p, "${") && strings.HasSuffix(p, "}") {
return p
}
//fmt.Println("=========>?", p)
//panic(p)
return ""
}
}
// 元素去重
func (this *Rule) setString(slice []string) []string {
// 解析位置
p := []string{}
for _, v := range slice {
if strings.Contains(v, "ARGS_NAMES_LIST") {
p = append(p, "uri")
} else if strings.Contains(v, "REQUEST_HEADER_FIELDS") {
p = append(p, "header")
} else if strings.Contains(v, "COOKIES_NAMES_LIST") {
p = append(p, "REQUEST_COOKIES_NAMES")
} else if strings.Contains(v, "") {
p = append(p, strings.Split(v, "")...)
} else if strings.Contains(v, ",") {
p = append(p, strings.Split(v, ",")...)
} else {
p = append(p, v)
}
}
slice = p
res := make([]string, 0, len(slice))
m := map[string]int{}
for _, v := range slice {
v = this.formatPosition(v)
_, ok := m[v]
if ok {
continue
}
if v == "${headers}" || v == "${headerNames}" { // headers 包含headersNames 如果存在headers时 headersName 可以忽略
_, ok1 := m["${headers}"]
idx2, ok2 := m["${headerNames}"]
if ok2 {
res[idx2] = "${headers}"
delete(m, "${headerNames}")
m["{headers}"] = idx2
continue
}
if ok1 {
continue
}
}
m[v] = len(res)
res = append(res, v)
}
return res
}
// 支持多条件
/*
uri:\/wp-login\.php且body:.{256,}
uri:\/goform\/_aslvl且body:SAPassword=W2402
*/
func parseRegulars(conditions string) ([]string, []string, error) {
getFieldFunc := func(s string) (func(string) (string, func(string) string), string) {
s = strings.ToLower(s)
switch s {
case "uri", "body", "host", "header", "headers", "head", "cookie", "referer":
return nil, s
case "user-agent", "ua":
return nil, "${userAgent}"
case "authorization":
return func(s string) (string, func(string) string) {
return "${headers}", func(s string) string {
return "authorization:" + s
}
}, ""
default:
return nil, ""
}
}
cdts := strings.Split(conditions, "且")
var regulars []string
var positions []string
for _, cdt := range cdts {
i := strings.Index(cdt, ":")
if i == -1 { // 错误
return nil, nil, errors.New("invalid " + cdt)
}
// 提取position
nextFc, field := getFieldFunc(cdt[:i])
var position, regular string
if nextFc == nil && field == "" { // 无法识别
return nil, nil, errors.New("invalid " + cdt)
}
if nextFc != nil {
field, getRegularFc := nextFc(cdt[i+1:])
if field == "" || getRegularFc == nil { // 无效正则
return nil, nil, errors.New("invalid " + cdt)
}
position = field
regular = getRegularFc(cdt[i+1:])
} else {
position = field
regular = cdt[i+1:]
}
regulars = append(regulars, regular)
positions = append(positions, position)
}
return regulars, positions, nil
}

View File

@@ -0,0 +1,34 @@
/*
@Author: 1usir
@Description:
@File: excelize_test
@Version: 1.0.0
@Date: 2024/2/21 17:37
*/
package exce
import (
"fmt"
"os"
"testing"
)
func TestParseRule(t *testing.T) {
f, err := os.Open("/Users/1usir/works/waf/open-waf/waf/EdgeAdmin/internal/utils/exce/WAF ALL.xlsx")
if err != nil {
t.Fatal(err)
}
_, rs, err := ParseRules(f)
if err != nil {
t.Fatal(err)
}
for _, v := range rs {
for _, arg := range v.Position {
if arg == "{responseBody}" && v.Outbound {
fmt.Println(v.Name, v.Type, v.Regular, v.Position, v.Inbound, v.Outbound)
}
}
}
}

View File

@@ -0,0 +1,162 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package executils
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"time"
)
type Cmd struct {
name string
args []string
env []string
dir string
ctx context.Context
timeout time.Duration
cancelFunc func()
captureStdout bool
captureStderr bool
stdout *bytes.Buffer
stderr *bytes.Buffer
rawCmd *exec.Cmd
}
func NewCmd(name string, args ...string) *Cmd {
return &Cmd{
name: name,
args: args,
}
}
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
return (&Cmd{
name: name,
args: args,
}).WithTimeout(timeout)
}
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
this.timeout = timeout
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
this.ctx = ctx
this.cancelFunc = cancelFunc
return this
}
func (this *Cmd) WithStdout() *Cmd {
this.captureStdout = true
return this
}
func (this *Cmd) WithStderr() *Cmd {
this.captureStderr = true
return this
}
func (this *Cmd) WithEnv(env []string) *Cmd {
this.env = env
return this
}
func (this *Cmd) WithDir(dir string) *Cmd {
this.dir = dir
return this
}
func (this *Cmd) Start() error {
var cmd = this.compose()
return cmd.Start()
}
func (this *Cmd) Wait() error {
var cmd = this.compose()
return cmd.Wait()
}
func (this *Cmd) Run() error {
if this.cancelFunc != nil {
defer this.cancelFunc()
}
var cmd = this.compose()
return cmd.Run()
}
func (this *Cmd) RawStdout() string {
if this.stdout != nil {
return this.stdout.String()
}
return ""
}
func (this *Cmd) Stdout() string {
return strings.TrimSpace(this.RawStdout())
}
func (this *Cmd) RawStderr() string {
if this.stderr != nil {
return this.stderr.String()
}
return ""
}
func (this *Cmd) Stderr() string {
return strings.TrimSpace(this.RawStderr())
}
func (this *Cmd) String() string {
if this.rawCmd != nil {
return this.rawCmd.String()
}
var newCmd = exec.Command(this.name, this.args...)
return newCmd.String()
}
func (this *Cmd) Process() *os.Process {
if this.rawCmd != nil {
return this.rawCmd.Process
}
return nil
}
func (this *Cmd) compose() *exec.Cmd {
if this.rawCmd != nil {
return this.rawCmd
}
if this.ctx != nil {
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
} else {
this.rawCmd = exec.Command(this.name, this.args...)
}
if this.env != nil {
this.rawCmd.Env = this.env
}
if len(this.dir) > 0 {
this.rawCmd.Dir = this.dir
}
if this.captureStdout {
this.stdout = &bytes.Buffer{}
this.rawCmd.Stdout = this.stdout
}
if this.captureStderr {
this.stderr = &bytes.Buffer{}
this.rawCmd.Stderr = this.stderr
}
return this.rawCmd
}

View File

@@ -0,0 +1,61 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package executils_test
import (
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"testing"
"time"
)
func TestNewTimeoutCmd_Sleep(t *testing.T) {
var cmd = executils.NewTimeoutCmd(1*time.Second, "sleep", "3")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestNewTimeoutCmd_Echo(t *testing.T) {
var cmd = executils.NewTimeoutCmd(10*time.Second, "echo", "-n", "hello")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestNewTimeoutCmd_Echo2(t *testing.T) {
var cmd = executils.NewCmd("echo", "hello")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("raw stdout:", cmd.RawStdout())
t.Log("stderr:", cmd.Stderr())
t.Log("raw stderr:", cmd.RawStderr())
}
func TestNewTimeoutCmd_Echo3(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestCmd_Process(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
err := cmd.Run()
t.Log("error:", err)
t.Log(cmd.Process())
}
func TestNewTimeoutCmd_String(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
t.Log("stdout:", cmd.String())
}

View File

@@ -0,0 +1,58 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build linux
package executils
import (
"golang.org/x/sys/unix"
"io/fs"
"os"
"os/exec"
"syscall"
)
// LookPath customize our LookPath() function, to work in broken $PATH environment variable
func LookPath(file string) (string, error) {
result, err := exec.LookPath(file)
if err == nil && len(result) > 0 {
return result, nil
}
// add common dirs contains executable files these may be excluded in $PATH environment variable
var binPaths = []string{
"/usr/sbin",
"/usr/bin",
"/usr/local/sbin",
"/usr/local/bin",
}
for _, binPath := range binPaths {
var fullPath = binPath + string(os.PathSeparator) + file
stat, err := os.Stat(fullPath)
if err != nil {
continue
}
if stat.IsDir() {
return "", syscall.EISDIR
}
var mode = stat.Mode()
if mode.IsDir() {
return "", syscall.EISDIR
}
err = syscall.Faccessat(unix.AT_FDCWD, fullPath, unix.X_OK, unix.AT_EACCESS)
if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) {
return fullPath, err
}
if mode&0111 != 0 {
return fullPath, nil
}
return "", fs.ErrPermission
}
return "", &exec.Error{
Name: file,
Err: exec.ErrNotFound,
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !linux
package executils
import "os/exec"
func LookPath(file string) (string, error) {
return exec.LookPath(file)
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/types"
"os/exec"
"runtime"
)
func AddPortsToFirewall(ports []int) {
for _, port := range ports {
// Linux
if runtime.GOOS == "linux" {
// firewalld
firewallCmd, _ := executils.LookPath("firewall-cmd")
if len(firewallCmd) > 0 {
err := exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp").Run()
if err == nil {
logs.Println("ADMIN_NODE", "add port '"+types.String(port)+"' to firewalld")
_ = exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp", "--permanent").Run()
}
}
}
}
}

View File

@@ -0,0 +1,174 @@
package utils
import (
"bytes"
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/iputils"
"github.com/iwind/TeaGo/types"
"net"
"strings"
)
// ParseIPValue 解析IP值
func ParseIPValue(value string) (newValue string, ipFrom string, ipTo string, ok bool) {
if len(value) == 0 {
return
}
newValue = value
// ip1-ip2
if strings.Contains(value, "-") {
var pieces = strings.Split(value, "-")
if len(pieces) != 2 {
return
}
ipFrom = strings.TrimSpace(pieces[0])
ipTo = strings.TrimSpace(pieces[1])
if !iputils.IsValid(ipFrom) || !iputils.IsValid(ipTo) {
return
}
if !iputils.IsSameVersion(ipFrom, ipTo) {
return
}
if iputils.CompareIP(ipFrom, ipTo) > 0 {
ipFrom, ipTo = ipTo, ipFrom
newValue = ipFrom + "-" + ipTo
}
ok = true
return
}
// ip/mask
if strings.Contains(value, "/") {
cidr, err := iputils.ParseCIDR(value)
if err != nil {
return
}
return newValue, cidr.From().String(), cidr.To().String(), true
}
// single value
if iputils.IsValid(value) {
ipFrom = value
ok = true
return
}
return
}
// ExtractIP 分解IP
// 只支持D段掩码的CIDR
// 最多只记录255个值
func ExtractIP(ipStrings string) ([]string, error) {
ipStrings = strings.ReplaceAll(ipStrings, " ", "")
// CIDR
if strings.Contains(ipStrings, "/") {
_, cidrNet, err := net.ParseCIDR(ipStrings)
if err != nil {
return nil, err
}
var index = strings.Index(ipStrings, "/")
var ipFrom = ipStrings[:index]
var bits = types.Int(ipStrings[index+1:])
if bits < 24 {
return nil, errors.New("CIDR bits should be greater than 24")
}
var ipv4 = net.ParseIP(ipFrom).To4()
if len(ipv4) == 0 {
return nil, errors.New("support IPv4 only")
}
var result = []string{}
ipv4[3] = 0 // 从0开始
for i := 0; i <= 255; i++ {
if cidrNet.Contains(ipv4) {
result = append(result, ipv4.String())
}
ipv4 = NextIP(ipv4)
}
return result, nil
}
// IP Range
if strings.Contains(ipStrings, "-") {
var index = strings.Index(ipStrings, "-")
var ipFromString = ipStrings[:index]
var ipToString = ipStrings[index+1:]
var ipFrom = net.ParseIP(ipFromString).To4()
if len(ipFrom) == 0 {
return nil, errors.New("invalid ip '" + ipFromString + "'")
}
var ipTo = net.ParseIP(ipToString).To4()
if len(ipTo) == 0 {
return nil, errors.New("invalid ip '" + ipToString + "'")
}
if bytes.Compare(ipFrom, ipTo) > 0 {
ipFrom, ipTo = ipTo, ipFrom
}
var result = []string{}
for i := 0; i < 255; i++ {
if bytes.Compare(ipFrom, ipTo) > 0 {
break
}
result = append(result, ipFrom.String())
ipFrom = NextIP(ipFrom)
}
return result, nil
}
return []string{ipStrings}, nil
}
// NextIP IP增加1
func NextIP(prevIP net.IP) net.IP {
var ip = make(net.IP, len(prevIP))
copy(ip, prevIP)
var index = len(ip) - 1
for {
if ip[index] == 255 {
ip[index] = 0
index--
if index < 0 {
break
}
} else {
ip[index]++
break
}
}
return ip
}
// IsLocalIP 判断是否为本地IP
// ip 是To4()或者To16()的结果
func IsLocalIP(ip net.IP) bool {
if ip == nil {
return false
}
if ip[0] == 127 ||
ip[0] == 10 ||
(ip[0] == 172 && ip[1]&0xf0 == 16) ||
(ip[0] == 192 && ip[1] == 168) {
return true
}
if ip.String() == "::1" {
return true
}
return false
}

View File

@@ -0,0 +1,30 @@
package utils
import (
"net"
"testing"
)
func TestExtractIP(t *testing.T) {
t.Log(ExtractIP("192.168.1.100"))
}
func TestExtractIP_CIDR(t *testing.T) {
t.Log(ExtractIP("192.168.2.100/24"))
}
func TestExtractIP_Range(t *testing.T) {
t.Log(ExtractIP("192.168.2.100 - 192.168.4.2"))
}
func TestNextIP(t *testing.T) {
for _, ip := range []string{"192.168.1.1", "0.0.0.0", "255.255.255.255", "192.168.2.255", "192.168.255.255"} {
t.Log(ip+":", NextIP(net.ParseIP(ip).To4()))
}
}
func TestNextIP_Copy(t *testing.T) {
var ip = net.ParseIP("192.168.1.100")
var nextIP = NextIP(ip)
t.Log(ip, nextIP)
}

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