This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

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,19 @@
package domainutils
import (
"regexp"
"strings"
)
// ValidateDomainFormat 校验域名格式
func ValidateDomainFormat(domain string) bool {
pieces := strings.Split(domain, ".")
for _, piece := range pieces {
// \p{Han} 中文unicode字符集
if !regexp.MustCompile(`^[\p{Han}a-z0-9-]+$`).MatchString(piece) {
return false
}
}
return true
}

View File

@@ -0,0 +1,18 @@
package utils
import (
"github.com/iwind/TeaGo/logs"
"strings"
)
func PrintError(err error) {
// TODO 记录调用的文件名、行数
logs.Println("[ERROR]" + err.Error())
}
func IsNotFound(err error) bool {
if err == nil {
return false
}
return strings.Contains(strings.ToUpper(err.Error()), "NOT FOUND")
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"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, _ := exec.LookPath("firewall-cmd")
if len(firewallCmd) > 0 {
err := exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp").Run()
if err == nil {
logs.Println("API_NODE", "add port '"+types.String(port)+"' to firewalld")
_ = exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp", "--permanent").Run()
}
}
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/iputils"
"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
}

View File

@@ -0,0 +1,60 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package utils
import (
"bytes"
"encoding/json"
"errors"
"reflect"
)
// JSONIsNull 判断JSON数据是否为null
func JSONIsNull(jsonData []byte) bool {
return len(jsonData) == 0 || bytes.Equal(jsonData, []byte("null"))
}
// JSONClone 使用JSON克隆对象
func JSONClone(v interface{}) (interface{}, error) {
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
var nv = reflect.New(reflect.TypeOf(v).Elem()).Interface()
err = json.Unmarshal(data, nv)
if err != nil {
return nil, err
}
return nv, nil
}
// JSONDecodeConfig 解码并重新编码
// 是为了去除原有JSON中不需要的数据
func JSONDecodeConfig(data []byte, ptr any) (encodeJSON []byte, err error) {
err = json.Unmarshal(data, ptr)
if err != nil {
return
}
encodeJSON, err = json.Marshal(ptr)
if err != nil {
return
}
// validate config
if ptr != nil {
config, ok := ptr.(interface {
Init() error
})
if ok {
initErr := config.Init()
if initErr != nil {
err = errors.New("validate config failed: " + initErr.Error())
}
}
}
return
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package utils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils"
"testing"
)
func TestJSONClone(t *testing.T) {
type A struct {
B int `json:"b"`
C string `json:"c"`
}
var a = &A{B: 123, C: "456"}
for i := 0; i < 5; i++ {
c, err := utils.JSONClone(a)
if err != nil {
t.Fatal(err)
}
t.Logf("%p, %#v", c, c)
}
}

View File

@@ -0,0 +1,174 @@
package utils
import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/miekg/dns"
"sync"
)
var sharedDNSClient *dns.Client
var sharedDNSConfig *dns.ClientConfig
func init() {
if !teaconst.IsMain {
return
}
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
logs.Println("ERROR: configure dns client failed: " + err.Error())
return
}
sharedDNSConfig = config
sharedDNSClient = &dns.Client{}
}
// LookupCNAME 查询CNAME记录
// TODO 可以设置使用的DNS主机地址
func LookupCNAME(host string) (string, error) {
if sharedDNSClient == nil {
return "", errors.New("could not find dns client")
}
var m = new(dns.Msg)
m.SetQuestion(host+".", dns.TypeCNAME)
m.RecursionDesired = true
var lastErr error
var success = false
var result = ""
var serverAddrs = sharedDNSConfig.Servers
{
var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/}
for _, publicDNSHost := range publicDNSHosts {
if !lists.ContainsString(serverAddrs, publicDNSHost) {
serverAddrs = append(serverAddrs, publicDNSHost)
}
}
}
var wg = &sync.WaitGroup{}
for _, serverAddr := range serverAddrs {
wg.Add(1)
go func(serverAddr string) {
defer wg.Done()
r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
if err != nil {
lastErr = err
return
}
success = true
if len(r.Answer) == 0 {
return
}
result = r.Answer[0].(*dns.CNAME).Target
}(serverAddr)
}
wg.Wait()
if success {
return result, nil
}
return "", lastErr
}
// LookupNS 查询NS记录
// TODO 可以设置使用的DNS主机地址
func LookupNS(host string) ([]string, error) {
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
var c = new(dns.Client)
var m = new(dns.Msg)
m.SetQuestion(host+".", dns.TypeNS)
m.RecursionDesired = true
var result = []string{}
var lastErr error
var hasValidServer = false
for _, serverAddr := range config.Servers {
r, _, err := c.Exchange(m, configutils.QuoteIP(serverAddr)+":"+config.Port)
if err != nil {
lastErr = err
continue
}
hasValidServer = true
if len(r.Answer) == 0 {
continue
}
for _, answer := range r.Answer {
result = append(result, answer.(*dns.NS).Ns)
}
break
}
if hasValidServer {
return result, nil
}
return nil, lastErr
}
// LookupTXT 获取CNAME
// TODO 可以设置使用的DNS主机地址
func LookupTXT(host string) ([]string, error) {
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
var c = new(dns.Client)
var m = new(dns.Msg)
m.SetQuestion(host+".", dns.TypeTXT)
m.RecursionDesired = true
var lastErr error
var result = []string{}
var hasValidServer = false
for _, serverAddr := range config.Servers {
r, _, err := c.Exchange(m, configutils.QuoteIP(serverAddr)+":"+config.Port)
if err != nil {
lastErr = err
continue
}
hasValidServer = true
if len(r.Answer) == 0 {
continue
}
for _, answer := range r.Answer {
result = append(result, answer.(*dns.TXT).Txt...)
}
break
}
if hasValidServer {
return result, nil
}
return nil, lastErr
}

View File

@@ -0,0 +1,23 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package utils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils"
"testing"
)
func TestLookupCNAME(t *testing.T) {
for _, domain := range []string{"www.yun4s.cn", "example.com", "goedge.cn"} {
result, err := utils.LookupCNAME(domain)
t.Log(domain, "=>", result, err)
}
}
func TestLookupNS(t *testing.T) {
t.Log(utils.LookupNS("goedge.cn"))
}
func TestLookupTXT(t *testing.T) {
t.Log(utils.LookupTXT("_acme-challenge.dl"))
}

View File

@@ -0,0 +1,12 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import "regexp"
var mobileRegex = regexp.MustCompile(`^1\d{10}$`)
// IsValidMobile validate mobile number
func IsValidMobile(mobile string) bool {
return mobileRegex.MatchString(mobile)
}

View File

@@ -0,0 +1,17 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestIsValidMobile(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsFalse(utils.IsValidMobile("138"))
a.IsFalse(utils.IsValidMobile("1382222"))
a.IsFalse(utils.IsValidMobile("1381234567890"))
a.IsTrue(utils.IsValidMobile("13812345678"))
}

View File

@@ -0,0 +1,143 @@
package numberutils
import (
"fmt"
"github.com/iwind/TeaGo/types"
"regexp"
"strconv"
"strings"
)
func FormatInt64(value int64) string {
return strconv.FormatInt(value, 10)
}
func FormatInt(value int) string {
return strconv.Itoa(value)
}
func Pow1024(n int) int64 {
if n <= 0 {
return 1
}
if n == 1 {
return 1024
}
return Pow1024(n-1) * 1024
}
func FormatBytes(bytes int64) string {
if bytes < Pow1024(1) {
return FormatInt64(bytes) + "B"
} else if bytes < Pow1024(2) {
return TrimZeroSuffix(fmt.Sprintf("%.2fKiB", float64(bytes)/float64(Pow1024(1))))
} else if bytes < Pow1024(3) {
return TrimZeroSuffix(fmt.Sprintf("%.2fMiB", float64(bytes)/float64(Pow1024(2))))
} else if bytes < Pow1024(4) {
return TrimZeroSuffix(fmt.Sprintf("%.2fGiB", float64(bytes)/float64(Pow1024(3))))
} else if bytes < Pow1024(5) {
return TrimZeroSuffix(fmt.Sprintf("%.2fTiB", float64(bytes)/float64(Pow1024(4))))
} else if bytes < Pow1024(6) {
return TrimZeroSuffix(fmt.Sprintf("%.2fPiB", float64(bytes)/float64(Pow1024(5))))
} else {
return TrimZeroSuffix(fmt.Sprintf("%.2fEiB", float64(bytes)/float64(Pow1024(6))))
}
}
func FormatBits(bits int64) string {
if bits < Pow1024(1) {
return FormatInt64(bits) + "bps"
} else if bits < Pow1024(2) {
return TrimZeroSuffix(fmt.Sprintf("%.4fKbps", float64(bits)/float64(Pow1024(1))))
} else if bits < Pow1024(3) {
return TrimZeroSuffix(fmt.Sprintf("%.4fMbps", float64(bits)/float64(Pow1024(2))))
} else if bits < Pow1024(4) {
return TrimZeroSuffix(fmt.Sprintf("%.4fGbps", float64(bits)/float64(Pow1024(3))))
} else if bits < Pow1024(5) {
return TrimZeroSuffix(fmt.Sprintf("%.4fTbps", float64(bits)/float64(Pow1024(4))))
} else if bits < Pow1024(6) {
return TrimZeroSuffix(fmt.Sprintf("%.4fPbps", float64(bits)/float64(Pow1024(5))))
} else {
return TrimZeroSuffix(fmt.Sprintf("%.4fEbps", float64(bits)/float64(Pow1024(6))))
}
}
func FormatCount(count int64) string {
if count < 1000 {
return types.String(count)
}
if count < 1000*1000 {
return fmt.Sprintf("%.1fK", float32(count)/1000)
}
if count < 1000*1000*1000 {
return fmt.Sprintf("%.1fM", float32(count)/1000/1000)
}
return fmt.Sprintf("%.1fB", float32(count)/1000/1000/1000)
}
func FormatFloat(f interface{}, decimal int) string {
if f == nil {
return ""
}
switch x := f.(type) {
case float32, float64:
var s = fmt.Sprintf("%."+types.String(decimal)+"f", x)
// 分隔
var dotIndex = strings.Index(s, ".")
if dotIndex > 0 {
var d = s[:dotIndex]
var f2 = s[dotIndex:]
f2 = strings.TrimRight(strings.TrimRight(f2, "0"), ".")
return formatDigit(d) + f2
}
return s
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return formatDigit(types.String(x))
case string:
return x
}
return ""
}
var decimalReg = regexp.MustCompile(`^(\d+\.\d+)([a-zA-Z]+)?$`)
// TrimZeroSuffix 去除小数数字尾部多余的0
func TrimZeroSuffix(s string) string {
var matches = decimalReg.FindStringSubmatch(s)
if len(matches) < 3 {
return s
}
return strings.TrimRight(strings.TrimRight(matches[1], "0"), ".") + matches[2]
}
func formatDigit(d string) string {
if len(d) == 0 {
return d
}
var prefix = ""
if d[0] < '0' || d[0] > '9' {
prefix = d[:1]
d = d[1:]
}
var l = len(d)
if l > 3 {
var pieces = l / 3
var commIndex = l - pieces*3
var d2 = ""
if commIndex > 0 {
d2 = d[:commIndex] + ", "
}
for i := 0; i < pieces; i++ {
d2 += d[commIndex+i*3 : commIndex+i*3+3]
if i != pieces-1 {
d2 += ", "
}
}
return prefix + d2
}
return prefix + d
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package otputils
import (
"net/url"
)
// FixIssuer fix issuer in otp provisioning url
func FixIssuer(urlString string) string {
u, err := url.Parse(urlString)
if err != nil {
return urlString
}
var query = u.Query()
if query != nil {
var issuerName = query.Get("issuer")
if len(issuerName) > 0 {
unescapedIssuerName, unescapeErr := url.QueryUnescape(issuerName)
if unescapeErr == nil {
query.Set("issuer", unescapedIssuerName)
u.RawQuery = query.Encode()
}
}
return u.String()
}
return urlString
}

View File

@@ -0,0 +1,18 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package otputils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils/otputils"
"testing"
)
func TestFixIssuer(t *testing.T) {
var beforeURL = "otpauth://totp/GoEdge%25E7%25AE%25A1%25E7%2590%2586%25E5%2591%2598%25E7%25B3%25BB%25E7%25BB%259F:admin?issuer=GoEdge%25E7%25AE%25A1%25E7%2590%2586%25E5%2591%2598%25E7%25B3%25BB%25E7%25BB%259F&secret=Q3J4WNOWBRFLP3HI"
var afterURL = otputils.FixIssuer(beforeURL)
t.Log(afterURL)
if beforeURL == afterURL {
t.Fatal("'afterURL' should not be equal to 'beforeURL'")
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package portalutils
import (
"github.com/TeaOSLab/EdgeUser/internal/remotelogs"
"github.com/iwind/TeaGo/Tea"
"io"
"os"
)
func HasPortalIndex() bool {
return len(checkPortalIndex()) > 0
}
func ReadPortalIndex(writer io.Writer) {
var indexPath = checkPortalIndex()
if len(indexPath) == 0 {
return
}
fp, err := os.Open(indexPath)
if err != nil {
remotelogs.Error("PORTAL", "read portal index failed: "+err.Error())
return
}
defer func() {
_ = fp.Close()
}()
_, _ = io.Copy(writer, fp)
}
func checkPortalIndex() string {
var indexes = []string{
"index.html",
}
for _, index := range indexes {
var path = Tea.Root + "/www/" + index
stat, err := os.Stat(path)
if err != nil {
continue
}
if stat.IsDir() {
continue
}
return path
}
return ""
}

View File

@@ -0,0 +1,12 @@
package utils
import (
"runtime/debug"
)
func Recover() {
e := recover()
if e != nil {
debug.PrintStack()
}
}

View File

@@ -0,0 +1,111 @@
package utils
import (
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
"github.com/iwind/TeaGo/logs"
"log"
"os"
"path/filepath"
"runtime"
"sync"
)
// 服务管理器
type ServiceManager struct {
Name string
Description string
fp *os.File
logger *log.Logger
onceLocker sync.Once
}
// 获取对象
func NewServiceManager(name, description string) *ServiceManager {
manager := &ServiceManager{
Name: name,
Description: description,
}
// root
manager.resetRoot()
return manager
}
// 设置服务
func (this *ServiceManager) setup() {
this.onceLocker.Do(func() {
logFile := files.NewFile(Tea.Root + "/logs/service.log")
if logFile.Exists() {
_ = logFile.Delete()
}
//logger
fp, err := os.OpenFile(Tea.Root+"/logs/service.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
logs.Error(err)
return
}
this.fp = fp
this.logger = log.New(fp, "", log.LstdFlags)
})
}
// 记录普通日志
func (this *ServiceManager) Log(msg string) {
this.setup()
if this.logger == nil {
return
}
this.logger.Println("[info]" + msg)
}
// 记录错误日志
func (this *ServiceManager) LogError(msg string) {
this.setup()
if this.logger == nil {
return
}
this.logger.Println("[error]" + msg)
}
// 关闭
func (this *ServiceManager) Close() error {
if this.fp != nil {
return this.fp.Close()
}
return nil
}
// 重置Root
func (this *ServiceManager) resetRoot() {
if !Tea.IsTesting() {
exePath, err := os.Executable()
if err != nil {
exePath = os.Args[0]
}
link, err := filepath.EvalSymlinks(exePath)
if err == nil {
exePath = link
}
fullPath, err := filepath.Abs(exePath)
if err == nil {
Tea.UpdateRoot(filepath.Dir(filepath.Dir(fullPath)))
}
}
Tea.SetPublicDir(Tea.Root + Tea.DS + "web" + Tea.DS + "public")
Tea.SetViewsDir(Tea.Root + Tea.DS + "web" + Tea.DS + "views")
Tea.SetTmpDir(Tea.Root + Tea.DS + "web" + Tea.DS + "tmp")
}
// 保持命令行窗口是打开的
func (this *ServiceManager) PauseWindow() {
if runtime.GOOS != "windows" {
return
}
b := make([]byte, 1)
_, _ = os.Stdin.Read(b)
}

View File

@@ -0,0 +1,160 @@
//go:build linux
// +build linux
package utils
import (
"errors"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
"os"
"os/exec"
"regexp"
)
var systemdServiceFile = "/etc/systemd/system/edge-user.service"
var initServiceFile = "/etc/init.d/" + teaconst.SystemdServiceName
// 安装服务
func (this *ServiceManager) Install(exePath string, args []string) error {
if os.Getgid() != 0 {
return errors.New("only root users can install the service")
}
systemd, err := exec.LookPath("systemctl")
if err != nil {
return this.installInitService(exePath, args)
}
return this.installSystemdService(systemd, exePath, args)
}
// 启动服务
func (this *ServiceManager) Start() error {
if os.Getgid() != 0 {
return errors.New("only root users can start the service")
}
if files.NewFile(systemdServiceFile).Exists() {
systemd, err := exec.LookPath("systemctl")
if err != nil {
return err
}
return exec.Command(systemd, "start", teaconst.SystemdServiceName+".service").Start()
}
return exec.Command("service", teaconst.ProcessName, "start").Start()
}
// 删除服务
func (this *ServiceManager) Uninstall() error {
if os.Getgid() != 0 {
return errors.New("only root users can uninstall the service")
}
if files.NewFile(systemdServiceFile).Exists() {
systemd, err := exec.LookPath("systemctl")
if err != nil {
return err
}
// disable service
_ = exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Start()
// reload
_ = exec.Command(systemd, "daemon-reload").Start()
return files.NewFile(systemdServiceFile).Delete()
}
f := files.NewFile(initServiceFile)
if f.Exists() {
return f.Delete()
}
return nil
}
// install init service
func (this *ServiceManager) installInitService(exePath string, args []string) error {
shortName := teaconst.SystemdServiceName
scriptFile := Tea.Root + "/scripts/" + shortName
if !files.NewFile(scriptFile).Exists() {
return errors.New("'scripts/" + shortName + "' file not exists")
}
data, err := os.ReadFile(scriptFile)
if err != nil {
return err
}
data = regexp.MustCompile("INSTALL_DIR=.+").ReplaceAll(data, []byte("INSTALL_DIR="+Tea.Root))
err = os.WriteFile(initServiceFile, data, 0777)
if err != nil {
return err
}
chkCmd, err := exec.LookPath("chkconfig")
if err != nil {
return err
}
err = exec.Command(chkCmd, "--add", teaconst.ProcessName).Start()
if err != nil {
return err
}
return nil
}
// install systemd service
func (this *ServiceManager) installSystemdService(systemd, exePath string, args []string) error {
shortName := teaconst.SystemdServiceName
longName := "GoEdge User" // TODO 将来可以修改
var startCmd = exePath + " daemon"
bashPath, _ := exec.LookPath("bash")
if len(bashPath) > 0 {
startCmd = bashPath + " -c \"" + startCmd + "\""
}
desc := `# Provides: ` + shortName + `
# Required-Start: $all
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop:
# Short-Description: ` + longName + ` Service
### END INIT INFO
[Unit]
Description=` + longName + ` Service
Before=shutdown.target
After=network-online.target
[Service]
Type=simple
Restart=always
RestartSec=1s
ExecStart=` + startCmd + `
ExecStop=` + exePath + ` stop
ExecReload=` + exePath + ` reload
[Install]
WantedBy=multi-user.target`
// write file
err := os.WriteFile(systemdServiceFile, []byte(desc), 0777)
if err != nil {
return err
}
// stop current systemd service if running
_ = exec.Command(systemd, "stop", shortName+".service").Start()
// reload
_ = exec.Command(systemd, "daemon-reload").Start()
// enable
cmd := exec.Command(systemd, "enable", shortName+".service")
return cmd.Run()
}

View File

@@ -0,0 +1,19 @@
//go:build !linux && !windows
// +build !linux,!windows
package utils
// 安装服务
func (this *ServiceManager) Install(exePath string, args []string) error {
return nil
}
// 启动服务
func (this *ServiceManager) Start() error {
return nil
}
// 删除服务
func (this *ServiceManager) Uninstall() error {
return nil
}

View File

@@ -0,0 +1,12 @@
package utils
import (
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"testing"
)
func TestServiceManager_Log(t *testing.T) {
manager := NewServiceManager(teaconst.ProductName, teaconst.ProductName+" Server")
manager.Log("Hello, World")
manager.LogError("Hello, World")
}

View File

@@ -0,0 +1,175 @@
//go:build windows
// +build windows
package utils
import (
"fmt"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/iwind/TeaGo/Tea"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"os/exec"
)
// 安装服务
func (this *ServiceManager) Install(exePath string, args []string) error {
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("connecting: %w please 'Run as administrator' again", err)
}
defer m.Disconnect()
s, err := m.OpenService(this.Name)
if err == nil {
s.Close()
return fmt.Errorf("service %s already exists", this.Name)
}
s, err = m.CreateService(this.Name, exePath, mgr.Config{
DisplayName: this.Name,
Description: this.Description,
StartType: windows.SERVICE_AUTO_START,
}, args...)
if err != nil {
return fmt.Errorf("creating: %w", err)
}
defer s.Close()
return nil
}
// 启动服务
func (this *ServiceManager) Start() error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(this.Name)
if err != nil {
return fmt.Errorf("could not access service: %w", err)
}
defer s.Close()
err = s.Start("service")
if err != nil {
return fmt.Errorf("could not start service: %w", err)
}
return nil
}
// 删除服务
func (this *ServiceManager) Uninstall() error {
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("connecting: %w please 'Run as administrator' again", err)
}
defer m.Disconnect()
s, err := m.OpenService(this.Name)
if err != nil {
return fmt.Errorf("open service: %w", err)
}
// shutdown service
_, err = s.Control(svc.Stop)
if err != nil {
fmt.Printf("shutdown service: %s\n", err.Error())
}
defer s.Close()
err = s.Delete()
if err != nil {
return fmt.Errorf("deleting: %w", err)
}
return nil
}
// 运行
func (this *ServiceManager) Run() {
err := svc.Run(this.Name, this)
if err != nil {
this.LogError(err.Error())
}
}
// 同服务管理器的交互
func (this *ServiceManager) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
changes <- svc.Status{
State: svc.StartPending,
}
changes <- svc.Status{
State: svc.Running,
Accepts: cmdsAccepted,
}
// start service
this.Log("start")
this.cmdStart()
loop:
for {
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
this.Log("cmd: Interrogate")
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
this.Log("cmd: Stop|Shutdown")
// stop service
this.cmdStop()
break loop
case svc.Pause:
this.Log("cmd: Pause")
// stop service
this.cmdStop()
changes <- svc.Status{
State: svc.Paused,
Accepts: cmdsAccepted,
}
case svc.Continue:
this.Log("cmd: Continue")
// start service
this.cmdStart()
changes <- svc.Status{
State: svc.Running,
Accepts: cmdsAccepted,
}
default:
this.LogError(fmt.Sprintf("unexpected control request #%d\r\n", c))
}
}
}
changes <- svc.Status{
State: svc.StopPending,
}
return
}
// 启动Web服务
func (this *ServiceManager) cmdStart() {
cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "start")
err := cmd.Start()
if err != nil {
this.LogError(err.Error())
}
}
// 停止Web服务
func (this *ServiceManager) cmdStop() {
cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "stop")
err := cmd.Start()
if err != nil {
this.LogError(err.Error())
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package sizes
const (
K int64 = 1024
M = 1024 * K
G = 1024 * M
T = 1024 * G
)

View File

@@ -0,0 +1,17 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package sizes_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils/sizes"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestSizes(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsTrue(sizes.K == 1024)
a.IsTrue(sizes.M == 1024*1024)
a.IsTrue(sizes.G == 1024*1024*1024)
a.IsTrue(sizes.T == 1024*1024*1024*1024)
}

View File

@@ -0,0 +1,31 @@
package utils
import (
"github.com/iwind/TeaGo/types"
"strings"
)
// format address
func FormatAddress(addr string) string {
if strings.HasSuffix(addr, "unix:") {
return addr
}
addr = strings.Replace(addr, " ", "", -1)
addr = strings.Replace(addr, "\t", "", -1)
addr = strings.Replace(addr, "", ":", -1)
addr = strings.TrimSpace(addr)
return addr
}
// 分割数字
func SplitNumbers(numbers string) (result []int64) {
if len(numbers) == 0 {
return
}
pieces := strings.Split(numbers, ",")
for _, piece := range pieces {
number := types.Int64(strings.TrimSpace(piece))
result = append(result, number)
}
return
}

View File

@@ -0,0 +1,67 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"github.com/iwind/TeaGo/lists"
"strings"
)
func FilterNotEmpty(item string) bool {
return len(item) > 0
}
func MapAddPrefixFunc(prefix string) func(item string) string {
return func(item string) string {
if !strings.HasPrefix(item, prefix) {
return prefix + item
}
return item
}
}
type StringsStream struct {
s []string
}
func NewStringsStream(s []string) *StringsStream {
return &StringsStream{s: s}
}
func (this *StringsStream) Map(f ...func(item string) string) *StringsStream {
for index, item := range this.s {
for _, f1 := range f {
item = f1(item)
}
this.s[index] = item
}
return this
}
func (this *StringsStream) Filter(f ...func(item string) bool) *StringsStream {
for _, f1 := range f {
var newStrings = []string{}
for _, item := range this.s {
if f1(item) {
newStrings = append(newStrings, item)
}
}
this.s = newStrings
}
return this
}
func (this *StringsStream) Unique() *StringsStream {
var newStrings = []string{}
for _, item := range this.s {
if !lists.ContainsString(newStrings, item) {
newStrings = append(newStrings, item)
}
}
this.s = newStrings
return this
}
func (this *StringsStream) Result() []string {
return this.s
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils"
"strings"
"testing"
)
func TestStringsStream_Filter(t *testing.T) {
var stream = utils.NewStringsStream([]string{"a", "b", "1", "2", "", "png", "a"})
stream.Filter(func(item string) bool {
return len(item) > 0
})
t.Log(stream.Result())
stream.Map(func(item string) string {
return "." + item
})
t.Log(stream.Result())
stream.Unique()
t.Log(stream.Result())
stream.Map(strings.ToUpper, strings.ToLower)
t.Log(stream.Result())
}

View File

@@ -0,0 +1,47 @@
package utils
import (
"time"
)
// 类似于time.Ticker但能够真正地停止
type Ticker struct {
raw *time.Ticker
S chan bool
C <-chan time.Time
isStopped bool
}
// 创建新Ticker
func NewTicker(duration time.Duration) *Ticker {
raw := time.NewTicker(duration)
return &Ticker{
raw: raw,
C: raw.C,
S: make(chan bool, 1),
}
}
// 查找下一个Tick
func (this *Ticker) Next() bool {
select {
case <-this.raw.C:
return true
case <-this.S:
return false
}
}
// 停止
func (this *Ticker) Stop() {
if this.isStopped {
return
}
this.isStopped = true
this.raw.Stop()
this.S <- true
}

View File

@@ -0,0 +1,55 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"errors"
"fmt"
"github.com/iwind/TeaGo/types"
"regexp"
)
// RangeTimes 计算时间点
func RangeTimes(timeFrom string, timeTo string, everyMinutes int32) (result []string, err error) {
if everyMinutes <= 0 {
return nil, errors.New("invalid 'everyMinutes'")
}
var reg = regexp.MustCompile(`^\d{4}$`)
if !reg.MatchString(timeFrom) {
return nil, errors.New("invalid timeFrom '" + timeFrom + "'")
}
if !reg.MatchString(timeTo) {
return nil, errors.New("invalid timeTo '" + timeTo + "'")
}
if timeFrom > timeTo {
// swap
timeFrom, timeTo = timeTo, timeFrom
}
var everyMinutesInt = int(everyMinutes)
var fromHour = types.Int(timeFrom[:2])
var fromMinute = types.Int(timeFrom[2:])
var toHour = types.Int(timeTo[:2])
var toMinute = types.Int(timeTo[2:])
if fromMinute%everyMinutesInt == 0 {
result = append(result, timeFrom)
}
for {
fromMinute += everyMinutesInt
if fromMinute > 59 {
fromHour += fromMinute / 60
fromMinute = fromMinute % 60
}
if fromHour > toHour || (fromHour == toHour && fromMinute > toMinute) {
break
}
result = append(result, fmt.Sprintf("%02d%02d", fromHour, fromMinute))
}
return
}

View File

@@ -0,0 +1,95 @@
package utils
import (
"archive/zip"
"errors"
"io"
"os"
)
type Unzip struct {
zipFile string
targetDir string
}
func NewUnzip(zipFile string, targetDir string) *Unzip {
return &Unzip{
zipFile: zipFile,
targetDir: targetDir,
}
}
func (this *Unzip) Run() error {
if len(this.zipFile) == 0 {
return errors.New("zip file should not be empty")
}
if len(this.targetDir) == 0 {
return errors.New("target dir should not be empty")
}
reader, err := zip.OpenReader(this.zipFile)
if err != nil {
return err
}
defer func() {
_ = reader.Close()
}()
for _, file := range reader.File {
var info = file.FileInfo()
var target = this.targetDir + "/" + file.Name
// 目录
if info.IsDir() {
stat, err := os.Stat(target)
if err != nil {
if !os.IsNotExist(err) {
return err
} else {
err = os.MkdirAll(target, info.Mode())
if err != nil {
return err
}
}
} else if !stat.IsDir() {
err = os.MkdirAll(target, info.Mode())
if err != nil {
return err
}
}
continue
}
// 文件
err = func(file *zip.File, target string) error {
fileReader, err := file.Open()
if err != nil {
return err
}
defer func() {
_ = fileReader.Close()
}()
// remove old
_ = os.Remove(target)
// create new
fileWriter, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.FileInfo().Mode())
if err != nil {
return err
}
defer func() {
_ = fileWriter.Close()
}()
_, err = io.Copy(fileWriter, fileReader)
return err
}(file, target)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,307 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
type UpgradeFileWriter struct {
rawWriter io.Writer
written int64
}
func NewUpgradeFileWriter(rawWriter io.Writer) *UpgradeFileWriter {
return &UpgradeFileWriter{rawWriter: rawWriter}
}
func (this *UpgradeFileWriter) Write(p []byte) (n int, err error) {
n, err = this.rawWriter.Write(p)
this.written += int64(n)
return
}
func (this *UpgradeFileWriter) TotalWritten() int64 {
return this.written
}
type UpgradeManager struct {
client *http.Client
component string
newVersion string
contentLength int64
isDownloading bool
writer *UpgradeFileWriter
body io.ReadCloser
isCancelled bool
downloadURL string
}
func NewUpgradeManager(component string, downloadURL string) *UpgradeManager {
return &UpgradeManager{
component: component,
downloadURL: downloadURL,
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
CheckRedirect: nil,
Jar: nil,
Timeout: 30 * time.Minute,
},
}
}
func (this *UpgradeManager) Start() error {
if this.isDownloading {
return errors.New("another process is running")
}
this.isDownloading = true
defer func() {
this.client.CloseIdleConnections()
this.isDownloading = false
}()
// 检查unzip
unzipExe, _ := exec.LookPath("unzip")
// 检查cp
cpExe, _ := exec.LookPath("cp")
if len(cpExe) == 0 {
return errors.New("can not find 'cp' command")
}
// 检查新版本
var downloadURL = this.downloadURL
if len(downloadURL) == 0 {
var url = teaconst.UpdatesURL
var osName = runtime.GOOS
if Tea.IsTesting() && osName == "darwin" {
osName = "linux"
}
url = strings.ReplaceAll(url, "${os}", osName)
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
url = strings.ReplaceAll(url, "${version}", teaconst.Version)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("create url request failed: %w", err)
}
req.Header.Set("User-Agent", "Edge-User/"+teaconst.Version)
resp, err := this.client.Do(req)
if err != nil {
return fmt.Errorf("read latest version failed: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return errors.New("read latest version failed: invalid response code '" + types.String(resp.StatusCode) + "'")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read latest version failed: %w", err)
}
var m = maps.Map{}
err = json.Unmarshal(data, &m)
if err != nil {
return fmt.Errorf("invalid response data: %w, origin data: %s", err, string(data))
}
var code = m.GetInt("code")
if code != 200 {
return errors.New(m.GetString("message"))
}
var dataMap = m.GetMap("data")
var downloadHost = dataMap.GetString("host")
var versions = dataMap.GetSlice("versions")
var downloadPath = ""
for _, component := range versions {
var componentMap = maps.NewMap(component)
if componentMap.Has("version") {
if componentMap.GetString("code") == this.component {
var version = componentMap.GetString("version")
if stringutil.VersionCompare(version, teaconst.Version) > 0 {
this.newVersion = version
downloadPath = componentMap.GetString("url")
break
}
}
}
}
if len(downloadPath) == 0 {
return errors.New("no latest version to download")
}
downloadURL = downloadHost + downloadPath
}
{
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
if err != nil {
return fmt.Errorf("create download request failed: %w", err)
}
req.Header.Set("User-Agent", "Edge-User/"+teaconst.Version)
resp, err := this.client.Do(req)
if err != nil {
return fmt.Errorf("download failed: '%s': %w", downloadURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return errors.New("download failed: " + downloadURL + ": invalid response code '" + types.String(resp.StatusCode) + "'")
}
this.contentLength = resp.ContentLength
this.body = resp.Body
// download to tmp
var tmpDir = os.TempDir()
var filename = filepath.Base(downloadURL)
var destFile = tmpDir + "/" + filename
_ = os.Remove(destFile)
fp, err := os.Create(destFile)
if err != nil {
return fmt.Errorf("create file failed: %w", err)
}
defer func() {
// 删除安装文件
_ = os.Remove(destFile)
}()
this.writer = NewUpgradeFileWriter(fp)
_, err = io.Copy(this.writer, resp.Body)
if err != nil {
_ = fp.Close()
if this.isCancelled {
return nil
}
return fmt.Errorf("download failed: %w", err)
}
_ = fp.Close()
// unzip
var unzipDir = tmpDir + "/edge-" + this.component + "-tmp"
stat, err := os.Stat(unzipDir)
if err == nil && stat.IsDir() {
err = os.RemoveAll(unzipDir)
if err != nil {
return fmt.Errorf("remove old dir '%s' failed: %w", unzipDir, err)
}
}
if len(unzipExe) > 0 {
var unzipCmd = exec.Command(unzipExe, "-q", "-o", destFile, "-d", unzipDir)
var unzipStderr = &bytes.Buffer{}
unzipCmd.Stderr = unzipStderr
err = unzipCmd.Run()
if err != nil {
return fmt.Errorf("unzip installation file failed: %w: %s", err, unzipStderr.String())
}
} else {
var unzipCmd = &Unzip{
zipFile: destFile,
targetDir: unzipDir,
}
err = unzipCmd.Run()
if err != nil {
return fmt.Errorf("unzip installation file failed: %w", err)
}
}
installationFiles, err := filepath.Glob(unzipDir + "/edge-" + this.component + "/*")
if err != nil {
return fmt.Errorf("lookup installation files failed: %w", err)
}
// cp to target dir
currentExe, err := os.Executable()
if err != nil {
return fmt.Errorf("reveal current executable file path failed: %w", err)
}
var targetDir = filepath.Dir(filepath.Dir(currentExe))
if !Tea.IsTesting() {
for _, installationFile := range installationFiles {
var cpCmd = exec.Command(cpExe, "-R", "-f", installationFile, targetDir)
var cpStderr = &bytes.Buffer{}
cpCmd.Stderr = cpStderr
err = cpCmd.Run()
if err != nil {
return errors.New("overwrite installation files failed: '" + cpCmd.String() + "': " + cpStderr.String())
}
}
}
// remove tmp
_ = os.RemoveAll(unzipDir)
}
return nil
}
func (this *UpgradeManager) IsDownloading() bool {
return this.isDownloading
}
func (this *UpgradeManager) Progress() float32 {
if this.contentLength <= 0 {
return -1
}
if this.writer == nil {
return -1
}
return float32(this.writer.TotalWritten()) / float32(this.contentLength)
}
func (this *UpgradeManager) NewVersion() string {
return this.newVersion
}
func (this *UpgradeManager) Cancel() error {
this.isCancelled = true
this.isDownloading = false
if this.body != nil {
_ = this.body.Close()
}
return nil
}

View File

@@ -0,0 +1,35 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils_test
import (
"github.com/TeaOSLab/EdgeUser/internal/utils"
"testing"
"time"
)
func TestNewUpgradeManager(t *testing.T) {
var manager = utils.NewUpgradeManager("user", "")
var ticker = time.NewTicker(2 * time.Second)
go func() {
for range ticker.C {
if manager.IsDownloading() {
t.Logf("%.2f%%", manager.Progress()*100)
}
}
}()
/**go func() {
time.Sleep(5 * time.Second)
if manager.IsDownloading() {
t.Log("cancel downloading")
_ = manager.Cancel()
}
}()**/
err := manager.Start()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,24 @@
package utils
import (
"encoding/binary"
"net"
"strings"
)
// VersionToLong 计算版本代号
func VersionToLong(version string) uint32 {
var countDots = strings.Count(version, ".")
if countDots == 2 {
version += ".0"
} else if countDots == 1 {
version += ".0.0"
} else if countDots == 0 {
version += ".0.0.0"
}
var ip = net.ParseIP(version)
if ip == nil || ip.To4() == nil {
return 0
}
return binary.BigEndian.Uint32(ip.To4())
}