Initial commit (code only without large binaries)
This commit is contained in:
72
EdgeAPI/internal/accesslogs/storage_base.go
Normal file
72
EdgeAPI/internal/accesslogs/storage_base.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseStorage struct {
|
||||
isOk bool
|
||||
version int
|
||||
firewallOnly bool
|
||||
}
|
||||
|
||||
func (this *BaseStorage) SetVersion(version int) {
|
||||
this.version = version
|
||||
}
|
||||
|
||||
func (this *BaseStorage) Version() int {
|
||||
return this.version
|
||||
}
|
||||
|
||||
func (this *BaseStorage) IsOk() bool {
|
||||
return this.isOk
|
||||
}
|
||||
|
||||
func (this *BaseStorage) SetOk(isOk bool) {
|
||||
this.isOk = isOk
|
||||
}
|
||||
|
||||
func (this *BaseStorage) SetFirewallOnly(firewallOnly bool) {
|
||||
this.firewallOnly = firewallOnly
|
||||
}
|
||||
|
||||
// Marshal 对日志进行编码
|
||||
func (this *BaseStorage) Marshal(accessLog *pb.HTTPAccessLog) ([]byte, error) {
|
||||
return json.Marshal(accessLog)
|
||||
}
|
||||
|
||||
// FormatVariables 格式化字符串中的变量
|
||||
func (this *BaseStorage) FormatVariables(s string) string {
|
||||
var now = time.Now()
|
||||
return configutils.ParseVariables(s, func(varName string) (value string) {
|
||||
switch varName {
|
||||
case "year":
|
||||
return strconv.Itoa(now.Year())
|
||||
case "month":
|
||||
return fmt.Sprintf("%02d", now.Month())
|
||||
case "week":
|
||||
_, week := now.ISOWeek()
|
||||
return fmt.Sprintf("%02d", week)
|
||||
case "day":
|
||||
return fmt.Sprintf("%02d", now.Day())
|
||||
case "hour":
|
||||
return fmt.Sprintf("%02d", now.Hour())
|
||||
case "minute":
|
||||
return fmt.Sprintf("%02d", now.Minute())
|
||||
case "second":
|
||||
return fmt.Sprintf("%02d", now.Second())
|
||||
case "date":
|
||||
return fmt.Sprintf("%d%02d%02d", now.Year(), now.Month(), now.Day())
|
||||
}
|
||||
|
||||
return varName
|
||||
})
|
||||
}
|
||||
101
EdgeAPI/internal/accesslogs/storage_command.go
Normal file
101
EdgeAPI/internal/accesslogs/storage_command.go
Normal file
@@ -0,0 +1,101 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CommandStorage 通过命令行存储
|
||||
type CommandStorage struct {
|
||||
BaseStorage
|
||||
|
||||
config *serverconfigs.AccessLogCommandStorageConfig
|
||||
|
||||
writeLocker sync.Mutex
|
||||
}
|
||||
|
||||
func NewCommandStorage(config *serverconfigs.AccessLogCommandStorageConfig) *CommandStorage {
|
||||
return &CommandStorage{config: config}
|
||||
}
|
||||
|
||||
func (this *CommandStorage) Config() interface{} {
|
||||
return this.config
|
||||
}
|
||||
|
||||
// Start 启动
|
||||
func (this *CommandStorage) Start() error {
|
||||
if len(this.config.Command) == 0 {
|
||||
return errors.New("'command' should not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
func (this *CommandStorage) Write(accessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(accessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
this.writeLocker.Lock()
|
||||
defer this.writeLocker.Unlock()
|
||||
|
||||
var cmd = exec.Command(this.config.Command, this.config.Args...)
|
||||
if len(this.config.Dir) > 0 {
|
||||
cmd.Dir = this.config.Dir
|
||||
}
|
||||
|
||||
var stdout = bytes.NewBuffer([]byte{})
|
||||
cmd.Stdout = stdout
|
||||
|
||||
w, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, accessLog := range accessLogs {
|
||||
if this.firewallOnly && accessLog.FirewallPolicyId == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := this.Marshal(accessLog)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
continue
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
}
|
||||
}
|
||||
_ = w.Close()
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
|
||||
if stdout.Len() > 0 {
|
||||
logs.Error(errors.New(stdout.String()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *CommandStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
65
EdgeAPI/internal/accesslogs/storage_command_test.go
Normal file
65
EdgeAPI/internal/accesslogs/storage_command_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
executils "github.com/TeaOSLab/EdgeAPI/internal/utils/exec"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCommandStorage_Write(t *testing.T) {
|
||||
php, err := executils.LookPath("php")
|
||||
if err != nil { // not found php, so we can not test
|
||||
t.Log("php:", err)
|
||||
return
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
|
||||
storage := NewCommandStorage(&serverconfigs.AccessLogCommandStorageConfig{
|
||||
Command: php,
|
||||
Args: []string{cwd + "/tests/command_storage.php"},
|
||||
})
|
||||
err = storage.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/hello",
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/world",
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/lu",
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/ping",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(time.Since(before).Seconds(), "seconds")
|
||||
}
|
||||
159
EdgeAPI/internal/accesslogs/storage_es.go
Normal file
159
EdgeAPI/internal/accesslogs/storage_es.go
Normal file
@@ -0,0 +1,159 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ESStorage ElasticSearch存储策略
|
||||
type ESStorage struct {
|
||||
BaseStorage
|
||||
|
||||
config *serverconfigs.AccessLogESStorageConfig
|
||||
}
|
||||
|
||||
func NewESStorage(config *serverconfigs.AccessLogESStorageConfig) *ESStorage {
|
||||
return &ESStorage{config: config}
|
||||
}
|
||||
|
||||
func (this *ESStorage) Config() any {
|
||||
return this.config
|
||||
}
|
||||
|
||||
// Start 开启
|
||||
func (this *ESStorage) Start() error {
|
||||
if len(this.config.Endpoint) == 0 {
|
||||
return errors.New("'endpoint' should not be nil")
|
||||
}
|
||||
if !regexp.MustCompile(`(?i)^(http|https)://`).MatchString(this.config.Endpoint) {
|
||||
this.config.Endpoint = "http://" + this.config.Endpoint
|
||||
}
|
||||
|
||||
// 去除endpoint中的路径部分
|
||||
u, err := url.Parse(this.config.Endpoint)
|
||||
if err == nil && len(u.Path) > 0 {
|
||||
this.config.Endpoint = u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
if len(this.config.Index) == 0 {
|
||||
return errors.New("'index' should not be nil")
|
||||
}
|
||||
if !this.config.IsDataStream && len(this.config.MappingType) == 0 {
|
||||
return errors.New("'mappingType' should not be nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
func (this *ESStorage) Write(accessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(accessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bulk = &strings.Builder{}
|
||||
var indexName = this.FormatVariables(this.config.Index)
|
||||
var typeName = this.FormatVariables(this.config.MappingType)
|
||||
for _, accessLog := range accessLogs {
|
||||
if this.firewallOnly && accessLog.FirewallPolicyId == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(accessLog.RequestId) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var indexMap = map[string]any{
|
||||
"_index": indexName,
|
||||
"_id": accessLog.RequestId,
|
||||
}
|
||||
if !this.config.IsDataStream {
|
||||
indexMap["_type"] = typeName
|
||||
}
|
||||
opData, err := json.Marshal(map[string]any{
|
||||
"index": indexMap,
|
||||
})
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_ES_STORAGE", "write failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := this.Marshal(accessLog)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_ES_STORAGE", "marshal data failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
bulk.Write(opData)
|
||||
bulk.WriteString("\n")
|
||||
bulk.Write(data)
|
||||
bulk.WriteString("\n")
|
||||
}
|
||||
|
||||
if bulk.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, this.config.Endpoint+"/_bulk", strings.NewReader(bulk.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", strings.ReplaceAll(teaconst.ProductName, " ", "-")+"/"+teaconst.Version)
|
||||
if len(this.config.Username) > 0 || len(this.config.Password) > 0 {
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(this.config.Username+":"+this.config.Password)))
|
||||
}
|
||||
var client = utils.SharedHttpClient(10 * time.Second)
|
||||
defer func() {
|
||||
_ = req.Body.Close()
|
||||
}()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyData, _ := io.ReadAll(resp.Body)
|
||||
return errors.New("ElasticSearch response status code: " + fmt.Sprintf("%d", resp.StatusCode) + " content: " + string(bodyData))
|
||||
}
|
||||
|
||||
bodyData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ElasticSearch response failed: %w", err)
|
||||
}
|
||||
var m = maps.Map{}
|
||||
err = json.Unmarshal(bodyData, &m)
|
||||
if err == nil {
|
||||
// 暂不处理非JSON的情况
|
||||
if m.Has("errors") && m.GetBool("errors") {
|
||||
return errors.New("ElasticSearch returns '" + string(bodyData) + "'")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *ESStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
55
EdgeAPI/internal/accesslogs/storage_es_test.go
Normal file
55
EdgeAPI/internal/accesslogs/storage_es_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestESStorage_Write(t *testing.T) {
|
||||
storage := NewESStorage(&serverconfigs.AccessLogESStorageConfig{
|
||||
Endpoint: "http://127.0.0.1:9200",
|
||||
Index: "logs",
|
||||
MappingType: "accessLogs",
|
||||
Username: "hello",
|
||||
Password: "world",
|
||||
})
|
||||
err := storage.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestMethod: "POST",
|
||||
RequestPath: "/1",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
TimeISO8601: "2018-07-23T22:23:35+08:00",
|
||||
Header: map[string]*pb.Strings{
|
||||
"Content-Type": {Values: []string{"text/html"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/2",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
TimeISO8601: "2018-07-23T22:23:35+08:00",
|
||||
Header: map[string]*pb.Strings{
|
||||
"Content-Type": {Values: []string{"text/css"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
132
EdgeAPI/internal/accesslogs/storage_file.go
Normal file
132
EdgeAPI/internal/accesslogs/storage_file.go
Normal file
@@ -0,0 +1,132 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FileStorage 文件存储策略
|
||||
type FileStorage struct {
|
||||
BaseStorage
|
||||
|
||||
config *serverconfigs.AccessLogFileStorageConfig
|
||||
|
||||
writeLocker sync.Mutex
|
||||
|
||||
files map[string]*os.File // path => *File
|
||||
filesLocker sync.Mutex
|
||||
}
|
||||
|
||||
func NewFileStorage(config *serverconfigs.AccessLogFileStorageConfig) *FileStorage {
|
||||
return &FileStorage{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *FileStorage) Config() interface{} {
|
||||
return this.config
|
||||
}
|
||||
|
||||
// Start 开启
|
||||
func (this *FileStorage) Start() error {
|
||||
if len(this.config.Path) == 0 {
|
||||
return errors.New("'path' should not be empty")
|
||||
}
|
||||
|
||||
this.files = map[string]*os.File{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write 写入日志
|
||||
func (this *FileStorage) Write(accessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(accessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fp := this.fp()
|
||||
if fp == nil {
|
||||
return errors.New("file pointer should not be nil")
|
||||
}
|
||||
this.writeLocker.Lock()
|
||||
defer this.writeLocker.Unlock()
|
||||
|
||||
for _, accessLog := range accessLogs {
|
||||
if this.firewallOnly && accessLog.FirewallPolicyId == 0 {
|
||||
continue
|
||||
}
|
||||
data, err := this.Marshal(accessLog)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
continue
|
||||
}
|
||||
_, err = fp.Write(data)
|
||||
if err != nil {
|
||||
_ = this.Close()
|
||||
break
|
||||
}
|
||||
_, _ = fp.WriteString("\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *FileStorage) Close() error {
|
||||
this.filesLocker.Lock()
|
||||
defer this.filesLocker.Unlock()
|
||||
|
||||
var resultErr error
|
||||
for _, f := range this.files {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
resultErr = err
|
||||
}
|
||||
}
|
||||
return resultErr
|
||||
}
|
||||
|
||||
func (this *FileStorage) fp() *os.File {
|
||||
path := this.FormatVariables(this.config.Path)
|
||||
|
||||
this.filesLocker.Lock()
|
||||
defer this.filesLocker.Unlock()
|
||||
fp, ok := this.files[path]
|
||||
if ok {
|
||||
return fp
|
||||
}
|
||||
|
||||
// 关闭其他的文件
|
||||
for _, f := range this.files {
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
// 是否创建文件目录
|
||||
if this.config.AutoCreate {
|
||||
dir := filepath.Dir(path)
|
||||
_, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打开新文件
|
||||
fp, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return nil
|
||||
}
|
||||
this.files[path] = fp
|
||||
|
||||
return fp
|
||||
}
|
||||
72
EdgeAPI/internal/accesslogs/storage_file_test.go
Normal file
72
EdgeAPI/internal/accesslogs/storage_file_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFileStorage_Write(t *testing.T) {
|
||||
storage := NewFileStorage(&serverconfigs.AccessLogFileStorageConfig{
|
||||
Path: Tea.Root + "/logs/access-${date}.log",
|
||||
})
|
||||
err := storage.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestPath: "/hello",
|
||||
},
|
||||
{
|
||||
RequestPath: "/world",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestPath: "/1",
|
||||
},
|
||||
{
|
||||
RequestPath: "/2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestMethod: "POST",
|
||||
RequestPath: "/1",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/2",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
34
EdgeAPI/internal/accesslogs/storage_interface.go
Normal file
34
EdgeAPI/internal/accesslogs/storage_interface.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
|
||||
// StorageInterface 日志存储接口
|
||||
type StorageInterface interface {
|
||||
// Version 获取版本
|
||||
Version() int
|
||||
|
||||
// SetVersion 设置版本
|
||||
SetVersion(version int)
|
||||
|
||||
// SetFirewallOnly 设置是否只处理防火墙相关的访问日志
|
||||
SetFirewallOnly(firewallOnly bool)
|
||||
|
||||
IsOk() bool
|
||||
|
||||
SetOk(ok bool)
|
||||
|
||||
// Config 获取配置
|
||||
Config() interface{}
|
||||
|
||||
// Start 开启
|
||||
Start() error
|
||||
|
||||
// Write 写入日志
|
||||
Write(accessLogs []*pb.HTTPAccessLog) error
|
||||
|
||||
// Close 关闭
|
||||
Close() error
|
||||
}
|
||||
260
EdgeAPI/internal/accesslogs/storage_manager.go
Normal file
260
EdgeAPI/internal/accesslogs/storage_manager.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedStorageManager = NewStorageManager()
|
||||
|
||||
type StorageManager struct {
|
||||
storageMap map[int64]StorageInterface // policyId => Storage
|
||||
|
||||
publicPolicyId int64
|
||||
disableDefaultDB bool
|
||||
writeTargets *serverconfigs.AccessLogWriteTargets // 公用策略的写入目标
|
||||
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
func NewStorageManager() *StorageManager {
|
||||
return &StorageManager{
|
||||
storageMap: map[int64]StorageInterface{},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *StorageManager) Start() {
|
||||
var ticker = time.NewTicker(1 * time.Minute)
|
||||
if Tea.IsTesting() {
|
||||
ticker = time.NewTicker(5 * time.Second)
|
||||
}
|
||||
|
||||
// 启动时执行一次
|
||||
var err = this.Loop()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "update error: "+err.Error())
|
||||
}
|
||||
|
||||
// 循环执行
|
||||
for range ticker.C {
|
||||
err := this.Loop()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "update error: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop 更新
|
||||
func (this *StorageManager) Loop() error {
|
||||
var tx *dbs.Tx
|
||||
|
||||
// 共用策略
|
||||
publicPolicyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.publicPolicyId = publicPolicyId
|
||||
|
||||
// 所有策略
|
||||
policies, err := models.SharedHTTPAccessLogPolicyDAO.FindAllEnabledAndOnPolicies(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var foundPolicy = false
|
||||
var policyIds = []int64{}
|
||||
for _, policy := range policies {
|
||||
if policy.IsOn {
|
||||
policyIds = append(policyIds, int64(policy.Id))
|
||||
|
||||
if int64(policy.Id) == publicPolicyId {
|
||||
this.disableDefaultDB = policy.DisableDefaultDB
|
||||
this.writeTargets = serverconfigs.ParseWriteTargetsFromPolicy(policy.WriteTargets, policy.Type, policy.DisableDefaultDB)
|
||||
foundPolicy = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundPolicy {
|
||||
this.disableDefaultDB = false
|
||||
this.writeTargets = nil
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 关闭不用的
|
||||
for policyId, storage := range this.storageMap {
|
||||
if !lists.ContainsInt64(policyIds, policyId) {
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "close '"+types.String(policyId)+"' failed: "+err.Error())
|
||||
}
|
||||
delete(this.storageMap, policyId)
|
||||
remotelogs.Println("ACCESS_LOG_STORAGE_MANAGER", "remove '"+types.String(policyId)+"'")
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
var policyId = int64(policy.Id)
|
||||
storage, ok := this.storageMap[policyId]
|
||||
if ok {
|
||||
// 检查配置是否有变更
|
||||
if types.Int(policy.Version) != storage.Version() {
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "close policy '"+types.String(policyId)+"' failed: "+err.Error())
|
||||
|
||||
// 继续往下执行
|
||||
}
|
||||
|
||||
if len(policy.Options) > 0 {
|
||||
err = json.Unmarshal(policy.Options, storage.Config())
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "unmarshal policy '"+types.String(policyId)+"' config failed: "+err.Error())
|
||||
storage.SetOk(false)
|
||||
continue
|
||||
}
|
||||
}
|
||||
this.applyFileStorageFallback(policy.Type, storage)
|
||||
|
||||
storage.SetVersion(types.Int(policy.Version))
|
||||
storage.SetFirewallOnly(policy.FirewallOnly == 1)
|
||||
err = storage.Start()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "start policy '"+types.String(policyId)+"' failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
storage.SetOk(true)
|
||||
remotelogs.Println("ACCESS_LOG_STORAGE_MANAGER", "restart policy '"+types.String(policyId)+"'")
|
||||
}
|
||||
} else {
|
||||
storage, err = this.createStorage(policy.Type, policy.Options)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "create policy '"+types.String(policyId)+"' failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
this.applyFileStorageFallback(policy.Type, storage)
|
||||
storage.SetVersion(types.Int(policy.Version))
|
||||
storage.SetFirewallOnly(policy.FirewallOnly == 1)
|
||||
this.storageMap[policyId] = storage
|
||||
err = storage.Start()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_STORAGE_MANAGER", "start policy '"+types.String(policyId)+"' failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
storage.SetOk(true)
|
||||
remotelogs.Println("ACCESS_LOG_STORAGE_MANAGER", "start policy '"+types.String(policyId)+"'")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *StorageManager) DisableDefaultDB() bool {
|
||||
return this.disableDefaultDB
|
||||
}
|
||||
|
||||
// WriteMySQL 公用策略是否写入 MySQL(以 writeTargets 为准,无则用 disableDefaultDB)
|
||||
func (this *StorageManager) WriteMySQL() bool {
|
||||
if this.writeTargets != nil {
|
||||
return this.writeTargets.MySQL
|
||||
}
|
||||
return !this.disableDefaultDB
|
||||
}
|
||||
|
||||
// WriteClickHouse 公用策略是否写入 ClickHouse(文件+Fluent Bit 或后续 API 直写)
|
||||
func (this *StorageManager) WriteClickHouse() bool {
|
||||
if this.writeTargets != nil {
|
||||
return this.writeTargets.ClickHouse
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WriteTargets 返回公用策略的写入目标(供节点配置注入等)
|
||||
func (this *StorageManager) WriteTargets() *serverconfigs.AccessLogWriteTargets {
|
||||
return this.writeTargets
|
||||
}
|
||||
|
||||
func (this *StorageManager) createStorage(storageType string, optionsJSON []byte) (StorageInterface, error) {
|
||||
if serverconfigs.IsFileBasedStorageType(storageType) {
|
||||
storageType = serverconfigs.AccessLogStorageTypeFile
|
||||
}
|
||||
switch storageType {
|
||||
case serverconfigs.AccessLogStorageTypeFile:
|
||||
var config = &serverconfigs.AccessLogFileStorageConfig{}
|
||||
if len(optionsJSON) > 0 {
|
||||
err := json.Unmarshal(optionsJSON, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewFileStorage(config), nil
|
||||
case serverconfigs.AccessLogStorageTypeES:
|
||||
var config = &serverconfigs.AccessLogESStorageConfig{}
|
||||
if len(optionsJSON) > 0 {
|
||||
err := json.Unmarshal(optionsJSON, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewESStorage(config), nil
|
||||
case serverconfigs.AccessLogStorageTypeTCP:
|
||||
var config = &serverconfigs.AccessLogTCPStorageConfig{}
|
||||
if len(optionsJSON) > 0 {
|
||||
err := json.Unmarshal(optionsJSON, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewTCPStorage(config), nil
|
||||
case serverconfigs.AccessLogStorageTypeSyslog:
|
||||
var config = &serverconfigs.AccessLogSyslogStorageConfig{}
|
||||
if len(optionsJSON) > 0 {
|
||||
err := json.Unmarshal(optionsJSON, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewSyslogStorage(config), nil
|
||||
case serverconfigs.AccessLogStorageTypeCommand:
|
||||
var config = &serverconfigs.AccessLogCommandStorageConfig{}
|
||||
if len(optionsJSON) > 0 {
|
||||
err := json.Unmarshal(optionsJSON, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewCommandStorage(config), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid policy type '" + storageType + "'")
|
||||
}
|
||||
|
||||
func (this *StorageManager) applyFileStorageFallback(policyType string, storage StorageInterface) {
|
||||
if !serverconfigs.IsFileBasedStorageType(policyType) {
|
||||
return
|
||||
}
|
||||
|
||||
config, ok := storage.Config().(*serverconfigs.AccessLogFileStorageConfig)
|
||||
if !ok || config == nil || strings.TrimSpace(config.Path) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// file_clickhouse / file_mysql_clickhouse 未填写 path 时回退到默认文件路径,避免启动失败。
|
||||
if policyType == serverconfigs.AccessLogStorageTypeFileClickhouse || policyType == serverconfigs.AccessLogStorageTypeFileMySQLClickhouse {
|
||||
config.Path = Tea.Root + "/logs/access-${date}.log"
|
||||
config.AutoCreate = true
|
||||
}
|
||||
}
|
||||
19
EdgeAPI/internal/accesslogs/storage_manager_test.go
Normal file
19
EdgeAPI/internal/accesslogs/storage_manager_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStorageManager_Loop(t *testing.T) {
|
||||
dbs.NotifyReady()
|
||||
|
||||
var storage = NewStorageManager()
|
||||
err := storage.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(storage.storageMap)
|
||||
}
|
||||
89
EdgeAPI/internal/accesslogs/storage_manager_write.go
Normal file
89
EdgeAPI/internal/accesslogs/storage_manager_write.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var httpAccessLogQueue = make(chan []*pb.HTTPAccessLog, 1024)
|
||||
|
||||
func init() {
|
||||
if !teaconst.IsMain {
|
||||
return
|
||||
}
|
||||
|
||||
// 多进程读取
|
||||
var threads = runtime.NumCPU() / 2
|
||||
if threads <= 0 {
|
||||
threads = 1
|
||||
}
|
||||
if threads > 16 {
|
||||
threads = 16
|
||||
}
|
||||
for i := 0; i < threads; i++ {
|
||||
goman.New(func() {
|
||||
for pbAccessLogs := range httpAccessLogQueue {
|
||||
var policyId = SharedStorageManager.publicPolicyId
|
||||
if policyId <= 0 {
|
||||
continue
|
||||
}
|
||||
_, _, err := SharedStorageManager.WriteToPolicy(policyId, pbAccessLogs)
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_ACCESS_LOG_POLICY", "write failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (this *StorageManager) Write(pbAccessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(pbAccessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var policyId = this.publicPolicyId
|
||||
if policyId <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case httpAccessLogQueue <- pbAccessLogs:
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteToPolicy 写入日志到策略
|
||||
func (this *StorageManager) WriteToPolicy(policyId int64, accessLogs []*pb.HTTPAccessLog) (success bool, failMessage string, err error) {
|
||||
if !teaconst.IsPlus {
|
||||
return false, "only works in plus version", nil
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
storage, ok := this.storageMap[policyId]
|
||||
this.locker.Unlock()
|
||||
|
||||
if !ok {
|
||||
return false, "the policy has not been started yet", nil
|
||||
}
|
||||
|
||||
if !storage.IsOk() {
|
||||
return false, "the policy failed to start", nil
|
||||
}
|
||||
|
||||
err = storage.Write(accessLogs)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("write access log to policy '%d' failed: %w", policyId, err)
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
150
EdgeAPI/internal/accesslogs/storage_syslog.go
Normal file
150
EdgeAPI/internal/accesslogs/storage_syslog.go
Normal file
@@ -0,0 +1,150 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
executils "github.com/TeaOSLab/EdgeAPI/internal/utils/exec"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SyslogStorageProtocol = string
|
||||
|
||||
const (
|
||||
SyslogStorageProtocolTCP SyslogStorageProtocol = "tcp"
|
||||
SyslogStorageProtocolUDP SyslogStorageProtocol = "udp"
|
||||
SyslogStorageProtocolNone SyslogStorageProtocol = "none"
|
||||
SyslogStorageProtocolSocket SyslogStorageProtocol = "socket"
|
||||
)
|
||||
|
||||
type SyslogStoragePriority = int
|
||||
|
||||
// SyslogStorage syslog存储策略
|
||||
type SyslogStorage struct {
|
||||
BaseStorage
|
||||
|
||||
config *serverconfigs.AccessLogSyslogStorageConfig
|
||||
|
||||
exe string
|
||||
}
|
||||
|
||||
func NewSyslogStorage(config *serverconfigs.AccessLogSyslogStorageConfig) *SyslogStorage {
|
||||
return &SyslogStorage{config: config}
|
||||
}
|
||||
|
||||
func (this *SyslogStorage) Config() interface{} {
|
||||
return this.config
|
||||
}
|
||||
|
||||
// Start 开启
|
||||
func (this *SyslogStorage) Start() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return errors.New("'syslog' storage only works on linux")
|
||||
}
|
||||
|
||||
exe, err := executils.LookPath("logger")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.exe = exe
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
func (this *SyslogStorage) Write(accessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(accessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var args = []string{}
|
||||
if len(this.config.Tag) > 0 {
|
||||
args = append(args, "-t", this.config.Tag)
|
||||
}
|
||||
|
||||
if this.config.Priority >= 0 {
|
||||
args = append(args, "-p", strconv.Itoa(this.config.Priority))
|
||||
}
|
||||
|
||||
switch this.config.Protocol {
|
||||
case SyslogStorageProtocolTCP:
|
||||
args = append(args, "-T")
|
||||
if len(this.config.ServerAddr) > 0 {
|
||||
args = append(args, "-n", this.config.ServerAddr)
|
||||
}
|
||||
if this.config.ServerPort > 0 {
|
||||
args = append(args, "-P", strconv.Itoa(this.config.ServerPort))
|
||||
}
|
||||
case SyslogStorageProtocolUDP:
|
||||
args = append(args, "-d")
|
||||
if len(this.config.ServerAddr) > 0 {
|
||||
args = append(args, "-n", this.config.ServerAddr)
|
||||
}
|
||||
if this.config.ServerPort > 0 {
|
||||
args = append(args, "-P", strconv.Itoa(this.config.ServerPort))
|
||||
}
|
||||
case SyslogStorageProtocolSocket:
|
||||
args = append(args, "-u")
|
||||
args = append(args, this.config.Socket)
|
||||
case SyslogStorageProtocolNone:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
args = append(args, "-S", "10240")
|
||||
|
||||
var cmd = exec.Command(this.exe, args...)
|
||||
var stderrBuffer = &bytes.Buffer{}
|
||||
cmd.Stderr = stderrBuffer
|
||||
|
||||
w, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, accessLog := range accessLogs {
|
||||
if this.firewallOnly && accessLog.FirewallPolicyId == 0 {
|
||||
continue
|
||||
}
|
||||
data, err := this.Marshal(accessLog)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_POLICY_SYSLOG", "marshal accesslog failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_POLICY_SYSLOG", "write accesslog failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.Close()
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("send syslog failed: %w, stderr: %s", err, stderrBuffer.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *SyslogStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
116
EdgeAPI/internal/accesslogs/storage_tcp.go
Normal file
116
EdgeAPI/internal/accesslogs/storage_tcp.go
Normal file
@@ -0,0 +1,116 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TCPStorage TCP存储策略
|
||||
type TCPStorage struct {
|
||||
BaseStorage
|
||||
|
||||
config *serverconfigs.AccessLogTCPStorageConfig
|
||||
|
||||
writeLocker sync.Mutex
|
||||
|
||||
connLocker sync.Mutex
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func NewTCPStorage(config *serverconfigs.AccessLogTCPStorageConfig) *TCPStorage {
|
||||
return &TCPStorage{config: config}
|
||||
}
|
||||
|
||||
func (this *TCPStorage) Config() interface{} {
|
||||
return this.config
|
||||
}
|
||||
|
||||
// Start 开启
|
||||
func (this *TCPStorage) Start() error {
|
||||
if len(this.config.Network) == 0 {
|
||||
return errors.New("'network' should not be empty")
|
||||
}
|
||||
if len(this.config.Addr) == 0 {
|
||||
return errors.New("'addr' should not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
func (this *TCPStorage) Write(accessLogs []*pb.HTTPAccessLog) error {
|
||||
if len(accessLogs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := this.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var conn = this.conn
|
||||
if conn == nil {
|
||||
return errors.New("connection should not be nil")
|
||||
}
|
||||
|
||||
this.writeLocker.Lock()
|
||||
defer this.writeLocker.Unlock()
|
||||
|
||||
for _, accessLog := range accessLogs {
|
||||
if this.firewallOnly && accessLog.FirewallPolicyId == 0 {
|
||||
continue
|
||||
}
|
||||
data, err := this.Marshal(accessLog)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
continue
|
||||
}
|
||||
_, err = conn.Write(data)
|
||||
if err != nil {
|
||||
_ = this.Close()
|
||||
break
|
||||
}
|
||||
_, err = conn.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
_ = this.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *TCPStorage) Close() error {
|
||||
this.connLocker.Lock()
|
||||
defer this.connLocker.Unlock()
|
||||
|
||||
if this.conn != nil {
|
||||
err := this.conn.Close()
|
||||
this.conn = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *TCPStorage) connect() error {
|
||||
this.connLocker.Lock()
|
||||
defer this.connLocker.Unlock()
|
||||
|
||||
if this.conn != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := net.Dial(this.config.Network, this.config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.conn = conn
|
||||
|
||||
return nil
|
||||
}
|
||||
74
EdgeAPI/internal/accesslogs/storage_tcp_test.go
Normal file
74
EdgeAPI/internal/accesslogs/storage_tcp_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
//go:build plus
|
||||
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTCPStorage_Write(t *testing.T) {
|
||||
go func() {
|
||||
server, err := net.Listen("tcp", "127.0.0.1:9981")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
conn, err := server.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
t.Log(string(buf[:n]))
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
_ = server.Close()
|
||||
}()
|
||||
|
||||
storage := NewTCPStorage(&serverconfigs.AccessLogTCPStorageConfig{
|
||||
Network: "tcp",
|
||||
Addr: "127.0.0.1:9981",
|
||||
})
|
||||
err := storage.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
err = storage.Write([]*pb.HTTPAccessLog{
|
||||
{
|
||||
RequestMethod: "POST",
|
||||
RequestPath: "/1",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
},
|
||||
{
|
||||
RequestMethod: "GET",
|
||||
RequestPath: "/2",
|
||||
TimeLocal: time.Now().Format("2/Jan/2006:15:04:05 -0700"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
err = storage.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
24
EdgeAPI/internal/accesslogs/tests/command_storage.php
Normal file
24
EdgeAPI/internal/accesslogs/tests/command_storage.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// test command storage
|
||||
|
||||
// open access log file
|
||||
$fp = fopen("/tmp/goedge-command-storage.log", "a+");
|
||||
|
||||
// read access logs from stdin
|
||||
$stdin = fopen("php://stdin", "r");
|
||||
while(true) {
|
||||
if (feof($stdin)) {
|
||||
break;
|
||||
}
|
||||
$line = fgets($stdin);
|
||||
|
||||
// write to access log file
|
||||
fwrite($fp, $line);
|
||||
}
|
||||
|
||||
// close file pointers
|
||||
fclose($fp);
|
||||
fclose($stdin);
|
||||
|
||||
?>
|
||||
8
EdgeAPI/internal/acme/account.go
Normal file
8
EdgeAPI/internal/acme/account.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package acme
|
||||
|
||||
type Account struct {
|
||||
EABKid string
|
||||
EABKey string
|
||||
}
|
||||
147
EdgeAPI/internal/acme/acme_test.go
Normal file
147
EdgeAPI/internal/acme/acme_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
acmelog "github.com/go-acme/lego/v4/log"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
// You'll need a user or account type that implements acme.User
|
||||
type MyUser struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (u *MyUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
func (u MyUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
type MyProvider struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (this *MyProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
this.t.Log("provider: domain:", domain, "fqdn:", fqdn, "value:", value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MyProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 参考 https://go-acme.github.io/lego/usage/library/
|
||||
func TestGenerate(t *testing.T) {
|
||||
acmelog.Logger = log.New(io.Discard, "", log.LstdFlags)
|
||||
|
||||
// 生成私钥
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
myUser := &MyUser{
|
||||
Email: "test1@teaos.cn",
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(myUser)
|
||||
config.CADirURL = "https://acme.zerossl.com/v2/DV90"
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = client.Challenge.SetDNS01Provider(&MyProvider{t: t})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{"teaos.com"},
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(certificates)
|
||||
}
|
||||
|
||||
func TestGenerate_EAB(t *testing.T) {
|
||||
acmelog.Logger = log.New(io.Discard, "", log.LstdFlags)
|
||||
|
||||
// 生成私钥
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
myUser := &MyUser{
|
||||
Email: "test1@teaos.cn",
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(myUser)
|
||||
config.CADirURL = "https://acme.zerossl.com/v2/DV90"
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = client.Challenge.SetDNS01Provider(&MyProvider{t: t})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
var reg *registration.Resource
|
||||
if client.GetExternalAccountRequired() {
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: "KID",
|
||||
HmacEncoded: "HAMC KEY",
|
||||
})
|
||||
} else {
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
myUser.Registration = reg
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{"teaos.cn"},
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(certificates)
|
||||
}
|
||||
3
EdgeAPI/internal/acme/auth_callback.go
Normal file
3
EdgeAPI/internal/acme/auth_callback.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package acme
|
||||
|
||||
type AuthCallback func(domain, token, keyAuth string)
|
||||
88
EdgeAPI/internal/acme/dns_provider.go
Normal file
88
EdgeAPI/internal/acme/dns_provider.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DNSProvider struct {
|
||||
raw dnsclients.ProviderInterface
|
||||
dnsDomain string
|
||||
|
||||
locker sync.Mutex
|
||||
deletedRecordNames []string
|
||||
}
|
||||
|
||||
func NewDNSProvider(raw dnsclients.ProviderInterface, dnsDomain string) *DNSProvider {
|
||||
return &DNSProvider{
|
||||
raw: raw,
|
||||
dnsDomain: dnsDomain,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
_ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
|
||||
var info = dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
var fqdn = info.EffectiveFQDN
|
||||
var value = info.Value
|
||||
|
||||
// 设置记录
|
||||
var index = strings.Index(fqdn, "."+this.dnsDomain)
|
||||
if index < 0 {
|
||||
return errors.New("invalid fqdn value")
|
||||
}
|
||||
var recordName = fqdn[:index]
|
||||
|
||||
// 先删除老的
|
||||
this.locker.Lock()
|
||||
var wasDeleted = lists.ContainsString(this.deletedRecordNames, recordName)
|
||||
this.locker.Unlock()
|
||||
|
||||
if !wasDeleted {
|
||||
records, err := this.raw.QueryRecords(this.dnsDomain, recordName, dnstypes.RecordTypeTXT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query DNS record failed: %w", err)
|
||||
}
|
||||
for _, record := range records {
|
||||
err = this.raw.DeleteRecord(this.dnsDomain, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
this.locker.Lock()
|
||||
this.deletedRecordNames = append(this.deletedRecordNames, recordName)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
// 添加新的
|
||||
err := this.raw.AddRecord(this.dnsDomain, &dnstypes.Record{
|
||||
Id: "",
|
||||
Name: recordName,
|
||||
Type: dnstypes.RecordTypeTXT,
|
||||
Value: value,
|
||||
Route: this.raw.DefaultRoute(),
|
||||
TTL: this.raw.MinTTL(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create DNS record failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return 2 * time.Minute, 2 * time.Second
|
||||
}
|
||||
|
||||
func (this *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
23
EdgeAPI/internal/acme/http_provider.go
Normal file
23
EdgeAPI/internal/acme/http_provider.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package acme
|
||||
|
||||
type HTTPProvider struct {
|
||||
onAuth AuthCallback
|
||||
}
|
||||
|
||||
func NewHTTPProvider(onAuth AuthCallback) *HTTPProvider {
|
||||
return &HTTPProvider{
|
||||
onAuth: onAuth,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *HTTPProvider) Present(domain, token, keyAuth string) error {
|
||||
if this.onAuth != nil {
|
||||
this.onAuth(domain, token, keyAuth)
|
||||
}
|
||||
//http01.ChallengePath()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
15
EdgeAPI/internal/acme/key.go
Normal file
15
EdgeAPI/internal/acme/key.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func ParsePrivateKeyFromBase64(base64String string) (interface{}, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return x509.ParsePKCS8PrivateKey(data)
|
||||
}
|
||||
24
EdgeAPI/internal/acme/providers.go
Normal file
24
EdgeAPI/internal/acme/providers.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package acme
|
||||
|
||||
const DefaultProviderCode = "letsencrypt"
|
||||
|
||||
type Provider struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
APIURL string `json:"apiURL"`
|
||||
TestAPIURL string `json:"testAPIURL"`
|
||||
RequireEAB bool `json:"requireEAB"`
|
||||
EABDescription string `json:"eabDescription"`
|
||||
}
|
||||
|
||||
func FindProviderWithCode(code string) *Provider {
|
||||
for _, provider := range FindAllProviders() {
|
||||
if provider.Code == code {
|
||||
return provider
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
24
EdgeAPI/internal/acme/providers_ext.go
Normal file
24
EdgeAPI/internal/acme/providers_ext.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build !plus
|
||||
|
||||
package acme
|
||||
|
||||
func FindAllProviders() []*Provider {
|
||||
return []*Provider{
|
||||
{
|
||||
Name: "Let's Encrypt",
|
||||
Code: DefaultProviderCode,
|
||||
Description: "非盈利组织Let's Encrypt提供的免费证书。",
|
||||
APIURL: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
RequireEAB: false,
|
||||
},
|
||||
{
|
||||
Name: "ZeroSSL",
|
||||
Code: "zerossl",
|
||||
Description: "相关文档 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>。",
|
||||
APIURL: "https://acme.zerossl.com/v2/DV90",
|
||||
RequireEAB: true,
|
||||
EABDescription: "在官网<a href=\"https://app.zerossl.com/developer\" target=\"_blank\">[Developer]</a>页面底部点击\"Generate\"按钮生成。",
|
||||
},
|
||||
}
|
||||
}
|
||||
67
EdgeAPI/internal/acme/providers_ext_plus.go
Normal file
67
EdgeAPI/internal/acme/providers_ext_plus.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package acme
|
||||
|
||||
import teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
|
||||
func FindAllProviders() []*Provider {
|
||||
var providers = []*Provider{
|
||||
{
|
||||
Name: "Let's Encrypt",
|
||||
Code: DefaultProviderCode,
|
||||
Description: "非盈利组织Let's Encrypt提供的免费证书。",
|
||||
APIURL: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
RequireEAB: false,
|
||||
},
|
||||
}
|
||||
|
||||
// 商业版
|
||||
if teaconst.IsPlus {
|
||||
providers = append(providers, []*Provider{
|
||||
{
|
||||
Name: "Buypass",
|
||||
Code: "buypass",
|
||||
Description: "Buypass提供的免费证书。",
|
||||
APIURL: "https://api.buypass.com/acme/directory",
|
||||
TestAPIURL: "https://api.test4.buypass.no/acme/directory",
|
||||
RequireEAB: false,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
providers = append(providers, []*Provider{
|
||||
{
|
||||
Name: "ZeroSSL",
|
||||
Code: "zerossl",
|
||||
Description: "官方相关文档 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>。",
|
||||
APIURL: "https://acme.zerossl.com/v2/DV90",
|
||||
RequireEAB: true,
|
||||
EABDescription: "在官网<a href=\"https://app.zerossl.com/developer\" target=\"_blank\">[Developer]</a>页面底部点击\"Generate\"按钮生成。",
|
||||
},
|
||||
}...)
|
||||
|
||||
// 商业版
|
||||
if teaconst.IsPlus {
|
||||
providers = append(providers, []*Provider{
|
||||
{
|
||||
Name: "SSL.com",
|
||||
Code: "sslcom",
|
||||
Description: "官方相关文档 <a href=\"https://www.ssl.com/guide/ssl-tls-certificate-issuance-and-revocation-with-acme/\" target=\"_blank\">https://www.ssl.com/guide/ssl-tls-certificate-issuance-and-revocation-with-acme/</a>。",
|
||||
APIURL: "https://acme.ssl.com/sslcom-dv-rsa",
|
||||
RequireEAB: true,
|
||||
EABDescription: "登录SSL.com后,点击Dashboard中的api credentials链接,可以查看和创建密钥,EAB Kid对应界面中的Account/ACME Key,EAB HMAC Key对应界面中的HMAC Key。",
|
||||
},
|
||||
{
|
||||
Name: "Google Cloud",
|
||||
Code: "googleCloud",
|
||||
Description: "官方相关文档 <a href=\"https://cloud.google.com/certificate-manager/docs/public-ca-tutorial\" target=\"_blank\">https://cloud.google.com/certificate-manager/docs/public-ca-tutorial</a>",
|
||||
APIURL: "https://dv.acme-v02.api.pki.goog/directory",
|
||||
RequireEAB: true,
|
||||
EABDescription: "请根据Google Cloud官方文档运行 <code-label>gcloud publicca external-account-keys create</code-label> 获得Kid(对应keyId)和Key(对应b64MacKey)。",
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
215
EdgeAPI/internal/acme/request.go
Normal file
215
EdgeAPI/internal/acme/request.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
acmelog "github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
debug bool
|
||||
|
||||
task *Task
|
||||
onAuth AuthCallback
|
||||
}
|
||||
|
||||
func NewRequest(task *Task) *Request {
|
||||
return &Request{
|
||||
task: task,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Request) Debug() {
|
||||
this.debug = true
|
||||
}
|
||||
|
||||
func (this *Request) OnAuth(onAuth AuthCallback) {
|
||||
this.onAuth = onAuth
|
||||
}
|
||||
|
||||
func (this *Request) Run() (certData []byte, keyData []byte, err error) {
|
||||
if this.task.Provider == nil {
|
||||
err = errors.New("provider should not be nil")
|
||||
return
|
||||
}
|
||||
if this.task.Provider.RequireEAB && this.task.Account == nil {
|
||||
err = errors.New("account should not be nil when provider require EAB")
|
||||
return
|
||||
}
|
||||
|
||||
switch this.task.AuthType {
|
||||
case AuthTypeDNS:
|
||||
return this.runDNS()
|
||||
case AuthTypeHTTP:
|
||||
return this.runHTTP()
|
||||
default:
|
||||
err = errors.New("invalid task type '" + this.task.AuthType + "'")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Request) runDNS() (certData []byte, keyData []byte, err error) {
|
||||
if !this.debug {
|
||||
if !Tea.IsTesting() {
|
||||
acmelog.Logger = log.New(io.Discard, "", log.LstdFlags)
|
||||
}
|
||||
}
|
||||
|
||||
if this.task.User == nil {
|
||||
err = errors.New("'user' must not be nil")
|
||||
return
|
||||
}
|
||||
if this.task.DNSProvider == nil {
|
||||
err = errors.New("'dnsProvider' must not be nil")
|
||||
return
|
||||
}
|
||||
if len(this.task.DNSDomain) == 0 {
|
||||
err = errors.New("'dnsDomain' must not be empty")
|
||||
return
|
||||
}
|
||||
if len(this.task.Domains) == 0 {
|
||||
err = errors.New("'domains' must not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
var config = lego.NewConfig(this.task.User)
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.CADirURL = this.task.Provider.APIURL
|
||||
config.UserAgent = teaconst.ProductName + "/" + teaconst.Version
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 注册用户
|
||||
var resource = this.task.User.GetRegistration()
|
||||
if resource != nil {
|
||||
_, err = client.Registration.QueryRegistration()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
if this.task.Provider.RequireEAB {
|
||||
resource, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: this.task.Account.EABKid,
|
||||
HmacEncoded: this.task.Account.EABKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("register user failed: %w", err)
|
||||
}
|
||||
err = this.task.User.Register(resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
resource, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = this.task.User.Register(resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Challenge.SetDNS01Provider(NewDNSProvider(this.task.DNSProvider, this.task.DNSDomain))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 申请证书
|
||||
var request = certificate.ObtainRequest{
|
||||
Domains: this.task.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
certResource, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("obtain cert failed: %w", err)
|
||||
}
|
||||
|
||||
return certResource.Certificate, certResource.PrivateKey, nil
|
||||
}
|
||||
|
||||
func (this *Request) runHTTP() (certData []byte, keyData []byte, err error) {
|
||||
if !this.debug {
|
||||
if !Tea.IsTesting() {
|
||||
acmelog.Logger = log.New(io.Discard, "", log.LstdFlags)
|
||||
}
|
||||
}
|
||||
|
||||
if this.task.User == nil {
|
||||
err = errors.New("'user' must not be nil")
|
||||
return
|
||||
}
|
||||
|
||||
var config = lego.NewConfig(this.task.User)
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.CADirURL = this.task.Provider.APIURL
|
||||
config.UserAgent = teaconst.ProductName + "/" + teaconst.Version
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 注册用户
|
||||
var resource = this.task.User.GetRegistration()
|
||||
if resource != nil {
|
||||
_, err = client.Registration.QueryRegistration()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
if this.task.Provider.RequireEAB {
|
||||
resource, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: this.task.Account.EABKid,
|
||||
HmacEncoded: this.task.Account.EABKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("register user failed: %w", err)
|
||||
}
|
||||
err = this.task.User.Register(resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
resource, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = this.task.User.Register(resource)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Challenge.SetHTTP01Provider(NewHTTPProvider(this.onAuth))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 申请证书
|
||||
var request = certificate.ObtainRequest{
|
||||
Domains: this.task.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
certResource, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return certResource.Certificate, certResource.PrivateKey, nil
|
||||
}
|
||||
109
EdgeAPI/internal/acme/request_test.go
Normal file
109
EdgeAPI/internal/acme/request_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequest_Run_DNS(t *testing.T) {
|
||||
privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := NewUser("19644627@qq.com", privateKey, func(resource *registration.Resource) error {
|
||||
resourceJSON, err := json.Marshal(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Log(string(resourceJSON))
|
||||
return nil
|
||||
})
|
||||
|
||||
regResource := []byte(`{"body":{"status":"valid","contact":["mailto:19644627@qq.com"]},"uri":"https://acme-v02.api.letsencrypt.org/acme/acct/103672877"}`)
|
||||
err = user.SetRegistration(regResource)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dnsProvider, err := testDNSPodProvider()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := NewRequest(&Task{
|
||||
User: user,
|
||||
AuthType: AuthTypeDNS,
|
||||
DNSProvider: dnsProvider,
|
||||
DNSDomain: "yun4s.cn",
|
||||
Domains: []string{"www.yun4s.cn"},
|
||||
})
|
||||
certData, keyData, err := req.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("cert:", string(certData))
|
||||
t.Log("key:", string(keyData))
|
||||
}
|
||||
|
||||
func TestRequest_Run_HTTP(t *testing.T) {
|
||||
privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := NewUser("19644627@qq.com", privateKey, func(resource *registration.Resource) error {
|
||||
resourceJSON, err := json.Marshal(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Log(string(resourceJSON))
|
||||
return nil
|
||||
})
|
||||
|
||||
regResource := []byte(`{"body":{"status":"valid","contact":["mailto:19644627@qq.com"]},"uri":"https://acme-v02.api.letsencrypt.org/acme/acct/103672877"}`)
|
||||
err = user.SetRegistration(regResource)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := NewRequest(&Task{
|
||||
User: user,
|
||||
AuthType: AuthTypeHTTP,
|
||||
Domains: []string{"teaos.cn", "www.teaos.cn", "meloy.cn"},
|
||||
})
|
||||
certData, keyData, err := req.runHTTP()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(certData))
|
||||
t.Log(string(keyData))
|
||||
}
|
||||
|
||||
func testDNSPodProvider() (dnsclients.ProviderInterface, error) {
|
||||
db, err := dbs.Default()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='dnspod' ORDER BY id DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiParams := maps.Map{}
|
||||
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider := &dnsclients.DNSPodProvider{}
|
||||
err = provider.Auth(apiParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
22
EdgeAPI/internal/acme/task.go
Normal file
22
EdgeAPI/internal/acme/task.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package acme
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
|
||||
|
||||
type AuthType = string
|
||||
|
||||
const (
|
||||
AuthTypeDNS AuthType = "dns"
|
||||
AuthTypeHTTP AuthType = "http"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Provider *Provider
|
||||
Account *Account
|
||||
User *User
|
||||
AuthType AuthType
|
||||
Domains []string
|
||||
|
||||
// DNS相关
|
||||
DNSProvider dnsclients.ProviderInterface
|
||||
DNSDomain string
|
||||
}
|
||||
49
EdgeAPI/internal/acme/user.go
Normal file
49
EdgeAPI/internal/acme/user.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
email string
|
||||
resource *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
registerFunc func(resource *registration.Resource) error
|
||||
}
|
||||
|
||||
func NewUser(email string, key crypto.PrivateKey, registerFunc func(resource *registration.Resource) error) *User {
|
||||
return &User{
|
||||
email: email,
|
||||
key: key,
|
||||
registerFunc: registerFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *User) GetEmail() string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
func (this *User) GetRegistration() *registration.Resource {
|
||||
return this.resource
|
||||
}
|
||||
|
||||
func (this *User) SetRegistration(resourceData []byte) error {
|
||||
resource := ®istration.Resource{}
|
||||
err := json.Unmarshal(resourceData, resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.resource = resource
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *User) GetPrivateKey() crypto.PrivateKey {
|
||||
return this.key
|
||||
}
|
||||
|
||||
func (this *User) Register(resource *registration.Resource) error {
|
||||
this.resource = resource
|
||||
return this.registerFunc(resource)
|
||||
}
|
||||
300
EdgeAPI/internal/apps/app_cmd.go
Normal file
300
EdgeAPI/internal/apps/app_cmd.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/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
|
||||
usage 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.usage = 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)
|
||||
|
||||
usage := this.usage
|
||||
fmt.Println("Usage:", "\n "+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()) {
|
||||
// 获取参数
|
||||
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
|
||||
}
|
||||
|
||||
// 日志
|
||||
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))
|
||||
}
|
||||
|
||||
// 重启
|
||||
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
|
||||
}
|
||||
6
EdgeAPI/internal/apps/directive.go
Normal file
6
EdgeAPI/internal/apps/directive.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package apps
|
||||
|
||||
type Directive struct {
|
||||
Arg string
|
||||
Callback func()
|
||||
}
|
||||
108
EdgeAPI/internal/apps/log_writer.go
Normal file
108
EdgeAPI/internal/apps/log_writer.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/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
|
||||
}
|
||||
147
EdgeAPI/internal/clickhouse/client.go
Normal file
147
EdgeAPI/internal/clickhouse/client.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client 通过 HTTP 接口执行只读查询(SELECT),返回 JSONEachRow 解析为 map 或结构体
|
||||
type Client struct {
|
||||
cfg *Config
|
||||
httpCli *http.Client
|
||||
}
|
||||
|
||||
// NewClient 使用共享配置创建客户端
|
||||
func NewClient() *Client {
|
||||
cfg := SharedConfig()
|
||||
transport := &http.Transport{}
|
||||
if cfg != nil && strings.EqualFold(cfg.Scheme, "https") {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: cfg.TLSSkipVerify,
|
||||
ServerName: cfg.TLSServerName,
|
||||
}
|
||||
}
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpCli: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsConfigured 是否已配置
|
||||
func (c *Client) IsConfigured() bool {
|
||||
return c.cfg != nil && c.cfg.IsConfigured()
|
||||
}
|
||||
|
||||
// Query 执行 SELECT,将每行 JSON 解析到 dest 切片;dest 元素类型需为 *struct 或 map
|
||||
func (c *Client) Query(ctx context.Context, query string, dest interface{}) error {
|
||||
if !c.IsConfigured() {
|
||||
return fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
// 强制 JSONEachRow 便于解析
|
||||
q := strings.TrimSpace(query)
|
||||
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
|
||||
query = q + " FORMAT JSONEachRow"
|
||||
}
|
||||
u := c.buildURL(query)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.cfg.User != "" || c.cfg.Password != "" {
|
||||
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
|
||||
}
|
||||
resp, err := c.httpCli.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
return decodeRows(dec, dest)
|
||||
}
|
||||
|
||||
// QueryRow 执行仅返回一行的查询,将结果解析到 dest(*struct 或 *map)
|
||||
func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) error {
|
||||
if !c.IsConfigured() {
|
||||
return fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
q := strings.TrimSpace(query)
|
||||
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
|
||||
query = q + " FORMAT JSONEachRow"
|
||||
}
|
||||
u := c.buildURL(query)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.cfg.User != "" || c.cfg.Password != "" {
|
||||
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
|
||||
}
|
||||
resp, err := c.httpCli.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
return decodeOneRow(dec, dest)
|
||||
}
|
||||
|
||||
func (c *Client) buildURL(query string) string {
|
||||
scheme := "http"
|
||||
if c.cfg != nil && strings.EqualFold(c.cfg.Scheme, "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
rawURL := fmt.Sprintf("%s://%s:%d/?query=%s&database=%s",
|
||||
scheme, c.cfg.Host, c.cfg.Port, url.QueryEscape(query), url.QueryEscape(c.cfg.Database))
|
||||
return rawURL
|
||||
}
|
||||
|
||||
// decodeRows 将 JSONEachRow 流解析到 slice;元素类型须为 *struct 或 *[]map[string]interface{}
|
||||
func decodeRows(dec *json.Decoder, dest interface{}) error {
|
||||
// dest 应为 *[]*SomeStruct 或 *[]map[string]interface{}
|
||||
switch d := dest.(type) {
|
||||
case *[]map[string]interface{}:
|
||||
*d = (*d)[:0]
|
||||
for {
|
||||
var row map[string]interface{}
|
||||
if err := dec.Decode(&row); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
*d = append(*d, row)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{} or implement decoder)")
|
||||
}
|
||||
}
|
||||
|
||||
func decodeOneRow(dec *json.Decoder, dest interface{}) error {
|
||||
switch d := dest.(type) {
|
||||
case *map[string]interface{}:
|
||||
if err := dec.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("clickhouse: unsupported dest type for QueryRow (use *map[string]interface{})")
|
||||
}
|
||||
}
|
||||
134
EdgeAPI/internal/clickhouse/config.go
Normal file
134
EdgeAPI/internal/clickhouse/config.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Package clickhouse 提供 ClickHouse 只读客户端,用于查询 logs_ingest(Fluent Bit 写入)。
|
||||
// 配置优先从后台页面(edgeSysSettings.clickhouseConfig)读取,其次 api.yaml,最后环境变量。
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/configs"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
envHost = "CLICKHOUSE_HOST"
|
||||
envPort = "CLICKHOUSE_PORT"
|
||||
envUser = "CLICKHOUSE_USER"
|
||||
envPassword = "CLICKHOUSE_PASSWORD"
|
||||
envDatabase = "CLICKHOUSE_DATABASE"
|
||||
envScheme = "CLICKHOUSE_SCHEME"
|
||||
defaultPort = 8443
|
||||
defaultDB = "default"
|
||||
defaultScheme = "https"
|
||||
)
|
||||
|
||||
var (
|
||||
sharedConfig *Config
|
||||
configOnce sync.Once
|
||||
configLocker sync.Mutex
|
||||
)
|
||||
|
||||
// Config ClickHouse 连接配置(仅查询,不从代码写库)
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
Scheme string
|
||||
TLSSkipVerify bool
|
||||
TLSServerName string
|
||||
}
|
||||
|
||||
// SharedConfig 返回全局配置(优先从后台 DB 读取,其次 api.yaml,最后环境变量)
|
||||
func SharedConfig() *Config {
|
||||
configLocker.Lock()
|
||||
defer configLocker.Unlock()
|
||||
if sharedConfig != nil {
|
||||
return sharedConfig
|
||||
}
|
||||
sharedConfig = loadConfig()
|
||||
return sharedConfig
|
||||
}
|
||||
|
||||
// ResetSharedConfig 清空缓存,下次 SharedConfig() 时重新从 DB/文件/环境变量加载(后台保存 ClickHouse 配置后调用)
|
||||
func ResetSharedConfig() {
|
||||
configLocker.Lock()
|
||||
defer configLocker.Unlock()
|
||||
sharedConfig = nil
|
||||
}
|
||||
|
||||
func loadConfig() *Config {
|
||||
cfg := &Config{Port: defaultPort, Database: defaultDB, Scheme: defaultScheme, TLSSkipVerify: true}
|
||||
// 1) 优先从后台页面配置(DB)读取
|
||||
if models.SharedSysSettingDAO != nil {
|
||||
if dbCfg, err := models.SharedSysSettingDAO.ReadClickHouseConfig(nil); err == nil && dbCfg != nil && dbCfg.Host != "" {
|
||||
cfg.Host = dbCfg.Host
|
||||
cfg.Port = dbCfg.Port
|
||||
cfg.User = dbCfg.User
|
||||
cfg.Password = dbCfg.Password
|
||||
cfg.Database = dbCfg.Database
|
||||
cfg.Scheme = normalizeScheme(dbCfg.Scheme)
|
||||
cfg.TLSSkipVerify = true
|
||||
cfg.TLSServerName = ""
|
||||
if cfg.Port <= 0 {
|
||||
cfg.Port = defaultPort
|
||||
}
|
||||
if cfg.Database == "" {
|
||||
cfg.Database = defaultDB
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
// 2) 其次 api.yaml
|
||||
apiConfig, err := configs.SharedAPIConfig()
|
||||
if err == nil && apiConfig != nil && apiConfig.ClickHouse != nil && apiConfig.ClickHouse.Host != "" {
|
||||
ch := apiConfig.ClickHouse
|
||||
cfg.Host = ch.Host
|
||||
cfg.Port = ch.Port
|
||||
cfg.User = ch.User
|
||||
cfg.Password = ch.Password
|
||||
cfg.Database = ch.Database
|
||||
cfg.Scheme = normalizeScheme(ch.Scheme)
|
||||
cfg.TLSSkipVerify = true
|
||||
cfg.TLSServerName = ""
|
||||
if cfg.Port <= 0 {
|
||||
cfg.Port = defaultPort
|
||||
}
|
||||
if cfg.Database == "" {
|
||||
cfg.Database = defaultDB
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
// 3) 最后环境变量
|
||||
cfg.Host = os.Getenv(envHost)
|
||||
cfg.User = os.Getenv(envUser)
|
||||
cfg.Password = os.Getenv(envPassword)
|
||||
cfg.Database = os.Getenv(envDatabase)
|
||||
if cfg.Database == "" {
|
||||
cfg.Database = defaultDB
|
||||
}
|
||||
cfg.Scheme = normalizeScheme(os.Getenv(envScheme))
|
||||
cfg.TLSServerName = ""
|
||||
if p := os.Getenv(envPort); p != "" {
|
||||
if v, err := strconv.Atoi(p); err == nil {
|
||||
cfg.Port = v
|
||||
}
|
||||
}
|
||||
cfg.TLSSkipVerify = true
|
||||
return cfg
|
||||
}
|
||||
|
||||
func normalizeScheme(scheme string) string {
|
||||
s := strings.ToLower(strings.TrimSpace(scheme))
|
||||
if s == "https" {
|
||||
return "https"
|
||||
}
|
||||
return defaultScheme
|
||||
}
|
||||
|
||||
// IsConfigured 是否已配置(Host 非空即视为启用 ClickHouse 查询)
|
||||
func (c *Config) IsConfigured() bool {
|
||||
return c != nil && c.Host != ""
|
||||
}
|
||||
413
EdgeAPI/internal/clickhouse/logs_ingest_store.go
Normal file
413
EdgeAPI/internal/clickhouse/logs_ingest_store.go
Normal file
@@ -0,0 +1,413 @@
|
||||
// Package clickhouse 提供 logs_ingest 表的只读查询(列表分页),用于访问日志列表优先走 ClickHouse。
|
||||
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
// LogsIngestRow 对应 ClickHouse logs_ingest 表的一行(用于 List 结果与 RowToPB)
|
||||
type LogsIngestRow struct {
|
||||
Timestamp time.Time
|
||||
NodeId uint64
|
||||
ClusterId uint64
|
||||
ServerId uint64
|
||||
Host string
|
||||
IP string
|
||||
Method string
|
||||
Path string
|
||||
Status uint16
|
||||
BytesIn uint64
|
||||
BytesOut uint64
|
||||
CostMs uint32
|
||||
UA string
|
||||
Referer string
|
||||
LogType string
|
||||
TraceId string
|
||||
FirewallPolicyId uint64
|
||||
FirewallRuleGroupId uint64
|
||||
FirewallRuleSetId uint64
|
||||
FirewallRuleId uint64
|
||||
RequestHeaders string
|
||||
RequestBody string
|
||||
ResponseHeaders string
|
||||
ResponseBody string
|
||||
}
|
||||
|
||||
// ListFilter 列表查询条件(与 ListHTTPAccessLogsRequest 对齐)
|
||||
type ListFilter struct {
|
||||
Day string
|
||||
HourFrom string
|
||||
HourTo string
|
||||
Size int64
|
||||
Reverse bool
|
||||
HasError bool
|
||||
HasFirewallPolicy bool
|
||||
FirewallPolicyId int64
|
||||
NodeId int64
|
||||
ClusterId int64
|
||||
LastRequestId string
|
||||
ServerIds []int64
|
||||
NodeIds []int64
|
||||
// 搜索条件
|
||||
Keyword string
|
||||
Ip string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// LogsIngestStore 封装对 logs_ingest 的只读列表查询
|
||||
type LogsIngestStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// NewLogsIngestStore 创建 store,内部使用共享 Client
|
||||
func NewLogsIngestStore() *LogsIngestStore {
|
||||
return &LogsIngestStore{client: NewClient()}
|
||||
}
|
||||
|
||||
// Client 返回底层 Client,供调用方判断 IsConfigured()
|
||||
func (s *LogsIngestStore) Client() *Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// List 按条件分页查询 logs_ingest,返回行、下一页游标(trace_id)与错误
|
||||
func (s *LogsIngestStore) List(ctx context.Context, f ListFilter) (rows []*LogsIngestRow, nextCursor string, err error) {
|
||||
if !s.client.IsConfigured() {
|
||||
return nil, "", fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
if f.Day == "" {
|
||||
return nil, "", fmt.Errorf("clickhouse: day required")
|
||||
}
|
||||
dayNumber, err := normalizeDayNumber(f.Day)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
table := "logs_ingest"
|
||||
if s.client.cfg.Database != "" && s.client.cfg.Database != "default" {
|
||||
table = quoteIdent(s.client.cfg.Database) + "." + quoteIdent("logs_ingest")
|
||||
} else {
|
||||
table = quoteIdent(table)
|
||||
}
|
||||
|
||||
conditions := []string{"toYYYYMMDD(timestamp) = " + strconv.Itoa(dayNumber)}
|
||||
if f.HourFrom != "" {
|
||||
if _, err := strconv.Atoi(f.HourFrom); err == nil {
|
||||
conditions = append(conditions, "toHour(timestamp) >= "+f.HourFrom)
|
||||
}
|
||||
}
|
||||
if f.HourTo != "" {
|
||||
if _, err := strconv.Atoi(f.HourTo); err == nil {
|
||||
conditions = append(conditions, "toHour(timestamp) <= "+f.HourTo)
|
||||
}
|
||||
}
|
||||
if len(f.ServerIds) > 0 {
|
||||
parts := make([]string, 0, len(f.ServerIds))
|
||||
for _, id := range f.ServerIds {
|
||||
parts = append(parts, strconv.FormatInt(id, 10))
|
||||
}
|
||||
conditions = append(conditions, "server_id IN ("+strings.Join(parts, ",")+")")
|
||||
}
|
||||
if len(f.NodeIds) > 0 {
|
||||
parts := make([]string, 0, len(f.NodeIds))
|
||||
for _, id := range f.NodeIds {
|
||||
parts = append(parts, strconv.FormatInt(id, 10))
|
||||
}
|
||||
conditions = append(conditions, "node_id IN ("+strings.Join(parts, ",")+")")
|
||||
}
|
||||
if f.NodeId > 0 {
|
||||
conditions = append(conditions, "node_id = "+strconv.FormatInt(f.NodeId, 10))
|
||||
}
|
||||
if f.ClusterId > 0 {
|
||||
conditions = append(conditions, "cluster_id = "+strconv.FormatInt(f.ClusterId, 10))
|
||||
}
|
||||
if f.HasFirewallPolicy {
|
||||
conditions = append(conditions, "firewall_policy_id > 0")
|
||||
}
|
||||
if f.HasError {
|
||||
conditions = append(conditions, "status >= 400")
|
||||
}
|
||||
if f.FirewallPolicyId > 0 {
|
||||
conditions = append(conditions, "firewall_policy_id = "+strconv.FormatInt(f.FirewallPolicyId, 10))
|
||||
}
|
||||
|
||||
// 搜索条件
|
||||
if f.Keyword != "" {
|
||||
keyword := escapeString(f.Keyword)
|
||||
// 在 host, path, ip, ua 中模糊搜索
|
||||
conditions = append(conditions, fmt.Sprintf("(host LIKE '%%%s%%' OR path LIKE '%%%s%%' OR ip LIKE '%%%s%%' OR ua LIKE '%%%s%%')", keyword, keyword, keyword, keyword))
|
||||
}
|
||||
if f.Ip != "" {
|
||||
conditions = append(conditions, "ip = '"+escapeString(f.Ip)+"'")
|
||||
}
|
||||
if f.Domain != "" {
|
||||
conditions = append(conditions, "host LIKE '%"+escapeString(f.Domain)+"%'")
|
||||
}
|
||||
|
||||
// 游标分页:使用 trace_id 作为游标
|
||||
// Reverse=false:历史向后翻页,查询更早的数据
|
||||
// Reverse=true:实时增量拉新,查询更新的数据
|
||||
if f.LastRequestId != "" {
|
||||
if f.Reverse {
|
||||
conditions = append(conditions, "trace_id > '"+escapeString(f.LastRequestId)+"'")
|
||||
} else {
|
||||
conditions = append(conditions, "trace_id < '"+escapeString(f.LastRequestId)+"'")
|
||||
}
|
||||
}
|
||||
|
||||
where := strings.Join(conditions, " AND ")
|
||||
// 默认按时间倒序(最新的在前面),与前端默认行为一致
|
||||
orderDir := "DESC"
|
||||
if f.Reverse {
|
||||
orderDir = "ASC"
|
||||
}
|
||||
limit := f.Size
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
orderBy := fmt.Sprintf("timestamp %s, trace_id %s", orderDir, orderDir)
|
||||
|
||||
// 列表查询不 SELECT 大字段(request_headers / request_body / response_headers / response_body),
|
||||
// 避免每次翻页读取 GB 级数据。详情查看时通过 FindByTraceId 单独获取。
|
||||
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id FROM %s WHERE %s ORDER BY %s LIMIT %d",
|
||||
table, where, orderBy, limit+1)
|
||||
|
||||
var rawRows []map[string]interface{}
|
||||
if err = s.client.Query(ctx, query, &rawRows); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
rows = make([]*LogsIngestRow, 0, len(rawRows))
|
||||
for _, m := range rawRows {
|
||||
r := mapToLogsIngestRow(m)
|
||||
if r != nil {
|
||||
rows = append(rows, r)
|
||||
}
|
||||
}
|
||||
if !f.Reverse {
|
||||
if len(rows) > int(limit) {
|
||||
nextCursor = rows[limit].TraceId
|
||||
rows = rows[:limit]
|
||||
}
|
||||
return rows, nextCursor, nil
|
||||
}
|
||||
|
||||
if len(rows) > int(limit) {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
nextCursor = rows[len(rows)-1].TraceId
|
||||
}
|
||||
|
||||
// 实时模式统一返回为“最新在前”,与前端显示和 MySQL 语义一致。
|
||||
for left, right := 0, len(rows)-1; left < right; left, right = left+1, right-1 {
|
||||
rows[left], rows[right] = rows[right], rows[left]
|
||||
}
|
||||
return rows, nextCursor, nil
|
||||
}
|
||||
|
||||
// FindByTraceId 按 trace_id 查询单条日志详情
|
||||
func (s *LogsIngestStore) FindByTraceId(ctx context.Context, traceId string) (*LogsIngestRow, error) {
|
||||
if !s.client.IsConfigured() {
|
||||
return nil, fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
if traceId == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
table := quoteIdent("logs_ingest")
|
||||
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id, request_headers, request_body, response_headers, response_body FROM %s WHERE trace_id = '%s' LIMIT 1",
|
||||
table, escapeString(traceId))
|
||||
|
||||
var rawRows []map[string]interface{}
|
||||
if err := s.client.Query(ctx, query, &rawRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawRows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return mapToLogsIngestRow(rawRows[0]), nil
|
||||
}
|
||||
|
||||
func quoteIdent(name string) string {
|
||||
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
|
||||
}
|
||||
|
||||
func escapeString(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "''")
|
||||
}
|
||||
|
||||
func normalizeDayNumber(day string) (int, error) {
|
||||
normalized := strings.TrimSpace(day)
|
||||
if normalized == "" {
|
||||
return 0, fmt.Errorf("clickhouse: day required")
|
||||
}
|
||||
normalized = strings.ReplaceAll(normalized, "-", "")
|
||||
if len(normalized) != 8 {
|
||||
return 0, fmt.Errorf("clickhouse: invalid day '%s'", day)
|
||||
}
|
||||
dayNumber, err := strconv.Atoi(normalized)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("clickhouse: invalid day '%s'", day)
|
||||
}
|
||||
return dayNumber, nil
|
||||
}
|
||||
|
||||
func mapToLogsIngestRow(m map[string]interface{}) *LogsIngestRow {
|
||||
r := &LogsIngestRow{}
|
||||
u64 := func(key string) uint64 {
|
||||
v, ok := m[key]
|
||||
if !ok || v == nil {
|
||||
return 0
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return uint64(x)
|
||||
case string:
|
||||
n, _ := strconv.ParseUint(x, 10, 64)
|
||||
return n
|
||||
case json.Number:
|
||||
n, _ := x.Int64()
|
||||
return uint64(n)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
u32 := func(key string) uint32 {
|
||||
return uint32(u64(key))
|
||||
}
|
||||
str := func(key string) string {
|
||||
if v, ok := m[key]; ok && v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
ts := func(key string) time.Time {
|
||||
v, ok := m[key]
|
||||
if ok && v != nil {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", x)
|
||||
return t
|
||||
case float64:
|
||||
return time.Unix(int64(x), 0)
|
||||
case json.Number:
|
||||
n, _ := x.Int64()
|
||||
return time.Unix(n, 0)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
r.Timestamp = ts("timestamp")
|
||||
r.NodeId = u64("node_id")
|
||||
r.ClusterId = u64("cluster_id")
|
||||
r.ServerId = u64("server_id")
|
||||
r.Host = str("host")
|
||||
r.IP = str("ip")
|
||||
r.Method = str("method")
|
||||
r.Path = str("path")
|
||||
r.Status = uint16(u64("status"))
|
||||
r.BytesIn = u64("bytes_in")
|
||||
r.BytesOut = u64("bytes_out")
|
||||
r.CostMs = u32("cost_ms")
|
||||
r.UA = str("ua")
|
||||
r.Referer = str("referer")
|
||||
r.LogType = str("log_type")
|
||||
r.TraceId = str("trace_id")
|
||||
r.FirewallPolicyId = u64("firewall_policy_id")
|
||||
r.FirewallRuleGroupId = u64("firewall_rule_group_id")
|
||||
r.FirewallRuleSetId = u64("firewall_rule_set_id")
|
||||
r.FirewallRuleId = u64("firewall_rule_id")
|
||||
r.RequestHeaders = str("request_headers")
|
||||
r.RequestBody = str("request_body")
|
||||
r.ResponseHeaders = str("response_headers")
|
||||
r.ResponseBody = str("response_body")
|
||||
return r
|
||||
}
|
||||
|
||||
// RowToPB 将 logs_ingest 一行转为 pb.HTTPAccessLog(列表展示用+详情展示)
|
||||
func RowToPB(r *LogsIngestRow) *pb.HTTPAccessLog {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
a := &pb.HTTPAccessLog{
|
||||
RequestId: r.TraceId,
|
||||
ServerId: int64(r.ServerId),
|
||||
NodeId: int64(r.NodeId),
|
||||
Timestamp: r.Timestamp.Unix(),
|
||||
Host: r.Host,
|
||||
RawRemoteAddr: r.IP,
|
||||
RemoteAddr: r.IP,
|
||||
RequestMethod: r.Method,
|
||||
RequestPath: r.Path,
|
||||
RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
|
||||
Scheme: "http", // 默认 http,日志中未存储实际值
|
||||
Proto: "HTTP/1.1", // 默认值,日志中未存储实际值
|
||||
Status: int32(r.Status),
|
||||
RequestLength: int64(r.BytesIn),
|
||||
BytesSent: int64(r.BytesOut),
|
||||
RequestTime: float64(r.CostMs) / 1000,
|
||||
UserAgent: r.UA,
|
||||
Referer: r.Referer,
|
||||
FirewallPolicyId: int64(r.FirewallPolicyId),
|
||||
FirewallRuleGroupId: int64(r.FirewallRuleGroupId),
|
||||
FirewallRuleSetId: int64(r.FirewallRuleSetId),
|
||||
FirewallRuleId: int64(r.FirewallRuleId),
|
||||
}
|
||||
if r.TimeISO8601() != "" {
|
||||
a.TimeISO8601 = r.TimeISO8601()
|
||||
}
|
||||
// TimeLocal: 用户友好的时间格式 (e.g., "2026-02-07 23:17:12")
|
||||
if !r.Timestamp.IsZero() {
|
||||
a.TimeLocal = r.Timestamp.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// 解析请求头 (JSON -> map[string]*pb.Strings)
|
||||
// ClickHouse 中存储的是 map[string]string 格式
|
||||
if r.RequestHeaders != "" {
|
||||
var headers map[string]string
|
||||
if err := json.Unmarshal([]byte(r.RequestHeaders), &headers); err == nil {
|
||||
a.Header = make(map[string]*pb.Strings)
|
||||
for k, v := range headers {
|
||||
a.Header[k] = &pb.Strings{Values: []string{v}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析响应头 (JSON -> map[string]*pb.Strings)
|
||||
if r.ResponseHeaders != "" {
|
||||
var headers map[string]string
|
||||
if err := json.Unmarshal([]byte(r.ResponseHeaders), &headers); err == nil {
|
||||
a.SentHeader = make(map[string]*pb.Strings)
|
||||
for k, v := range headers {
|
||||
a.SentHeader[k] = &pb.Strings{Values: []string{v}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 请求体
|
||||
if r.RequestBody != "" {
|
||||
a.RequestBody = []byte(r.RequestBody)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TimeISO8601 便于 RowToPB 使用
|
||||
func (r *LogsIngestRow) TimeISO8601() string {
|
||||
if r.Timestamp.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return r.Timestamp.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
377
EdgeAPI/internal/clickhouse/ns_logs_ingest_store.go
Normal file
377
EdgeAPI/internal/clickhouse/ns_logs_ingest_store.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
// NSLogsIngestRow 对应 dns_logs_ingest 表一行.
|
||||
type NSLogsIngestRow struct {
|
||||
Timestamp time.Time
|
||||
RequestId string
|
||||
NodeId uint64
|
||||
ClusterId uint64
|
||||
DomainId uint64
|
||||
RecordId uint64
|
||||
RemoteAddr string
|
||||
QuestionName string
|
||||
QuestionType string
|
||||
RecordName string
|
||||
RecordType string
|
||||
RecordValue string
|
||||
Networking string
|
||||
IsRecursive bool
|
||||
Error string
|
||||
NSRouteCodes []string
|
||||
ContentJSON string
|
||||
}
|
||||
|
||||
// NSListFilter DNS 日志查询过滤条件.
|
||||
type NSListFilter struct {
|
||||
Day string
|
||||
Size int64
|
||||
Reverse bool
|
||||
LastRequestId string
|
||||
NSClusterId int64
|
||||
NSNodeId int64
|
||||
NSDomainId int64
|
||||
NSRecordId int64
|
||||
RecordType string
|
||||
Keyword string
|
||||
}
|
||||
|
||||
// NSLogsIngestStore DNS ClickHouse 查询封装.
|
||||
type NSLogsIngestStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// NewNSLogsIngestStore 创建 NSLogsIngestStore.
|
||||
func NewNSLogsIngestStore() *NSLogsIngestStore {
|
||||
return &NSLogsIngestStore{client: NewClient()}
|
||||
}
|
||||
|
||||
// Client 返回底层 client.
|
||||
func (s *NSLogsIngestStore) Client() *Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// List 列出 DNS 访问日志,返回列表、下一游标与是否有更多.
|
||||
func (s *NSLogsIngestStore) List(ctx context.Context, f NSListFilter) (rows []*NSLogsIngestRow, nextCursor string, hasMore bool, err error) {
|
||||
if !s.client.IsConfigured() {
|
||||
return nil, "", false, fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
if f.Day == "" {
|
||||
return nil, "", false, fmt.Errorf("clickhouse: day required")
|
||||
}
|
||||
dayNumber, err := normalizeDayNumber(f.Day)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
table := quoteIdent("dns_logs_ingest")
|
||||
if s.client.cfg.Database != "" && s.client.cfg.Database != "default" {
|
||||
table = quoteIdent(s.client.cfg.Database) + "." + quoteIdent("dns_logs_ingest")
|
||||
}
|
||||
|
||||
conditions := []string{"toYYYYMMDD(timestamp) = " + strconv.Itoa(dayNumber)}
|
||||
if f.NSClusterId > 0 {
|
||||
conditions = append(conditions, "cluster_id = "+strconv.FormatInt(f.NSClusterId, 10))
|
||||
}
|
||||
if f.NSNodeId > 0 {
|
||||
conditions = append(conditions, "node_id = "+strconv.FormatInt(f.NSNodeId, 10))
|
||||
}
|
||||
if f.NSDomainId > 0 {
|
||||
conditions = append(conditions, "domain_id = "+strconv.FormatInt(f.NSDomainId, 10))
|
||||
}
|
||||
if f.NSRecordId > 0 {
|
||||
conditions = append(conditions, "record_id = "+strconv.FormatInt(f.NSRecordId, 10))
|
||||
}
|
||||
if f.RecordType != "" {
|
||||
conditions = append(conditions, "question_type = '"+escapeString(f.RecordType)+"'")
|
||||
}
|
||||
if f.Keyword != "" {
|
||||
keyword := escapeString(f.Keyword)
|
||||
conditions = append(conditions, fmt.Sprintf("(remote_addr LIKE '%%%s%%' OR question_name LIKE '%%%s%%' OR record_value LIKE '%%%s%%' OR error LIKE '%%%s%%')", keyword, keyword, keyword, keyword))
|
||||
}
|
||||
|
||||
// 游标分页:reverse=false 查更旧,reverse=true 查更新。
|
||||
if f.LastRequestId != "" {
|
||||
if f.Reverse {
|
||||
conditions = append(conditions, "request_id > '"+escapeString(f.LastRequestId)+"'")
|
||||
} else {
|
||||
conditions = append(conditions, "request_id < '"+escapeString(f.LastRequestId)+"'")
|
||||
}
|
||||
}
|
||||
|
||||
orderDir := "DESC"
|
||||
if f.Reverse {
|
||||
orderDir = "ASC"
|
||||
}
|
||||
|
||||
limit := f.Size
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
// 列表查询不 SELECT content_json 大字段,减少翻页时的数据传输量。
|
||||
// 详情查看时通过 FindByRequestId 单独获取完整信息。
|
||||
query := fmt.Sprintf(
|
||||
"SELECT timestamp, request_id, node_id, cluster_id, domain_id, record_id, remote_addr, question_name, question_type, record_name, record_type, record_value, networking, is_recursive, error, ns_route_codes FROM %s WHERE %s ORDER BY timestamp %s, request_id %s LIMIT %d",
|
||||
table,
|
||||
strings.Join(conditions, " AND "),
|
||||
orderDir,
|
||||
orderDir,
|
||||
limit+1,
|
||||
)
|
||||
|
||||
var rawRows []map[string]interface{}
|
||||
if err = s.client.Query(ctx, query, &rawRows); err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
rows = make([]*NSLogsIngestRow, 0, len(rawRows))
|
||||
for _, rawRow := range rawRows {
|
||||
row := mapToNSLogsIngestRow(rawRow)
|
||||
if row != nil {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
|
||||
hasMore = len(rows) > int(limit)
|
||||
if hasMore {
|
||||
nextCursor = rows[limit].RequestId
|
||||
rows = rows[:limit]
|
||||
} else if len(rows) > 0 {
|
||||
nextCursor = rows[len(rows)-1].RequestId
|
||||
}
|
||||
|
||||
if f.Reverse {
|
||||
for left, right := 0, len(rows)-1; left < right; left, right = left+1, right-1 {
|
||||
rows[left], rows[right] = rows[right], rows[left]
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
nextCursor = rows[0].RequestId
|
||||
}
|
||||
}
|
||||
|
||||
return rows, nextCursor, hasMore, nil
|
||||
}
|
||||
|
||||
// FindByRequestId 按 request_id 查单条 DNS 日志.
|
||||
func (s *NSLogsIngestStore) FindByRequestId(ctx context.Context, requestId string) (*NSLogsIngestRow, error) {
|
||||
if !s.client.IsConfigured() {
|
||||
return nil, fmt.Errorf("clickhouse: not configured")
|
||||
}
|
||||
if requestId == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
table := quoteIdent("dns_logs_ingest")
|
||||
if s.client.cfg.Database != "" && s.client.cfg.Database != "default" {
|
||||
table = quoteIdent(s.client.cfg.Database) + "." + quoteIdent("dns_logs_ingest")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
"SELECT timestamp, request_id, node_id, cluster_id, domain_id, record_id, remote_addr, question_name, question_type, record_name, record_type, record_value, networking, is_recursive, error, ns_route_codes, content_json FROM %s WHERE request_id = '%s' LIMIT 1",
|
||||
table,
|
||||
escapeString(requestId),
|
||||
)
|
||||
|
||||
var rawRows []map[string]interface{}
|
||||
if err := s.client.Query(ctx, query, &rawRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawRows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return mapToNSLogsIngestRow(rawRows[0]), nil
|
||||
}
|
||||
|
||||
func mapToNSLogsIngestRow(row map[string]interface{}) *NSLogsIngestRow {
|
||||
result := &NSLogsIngestRow{}
|
||||
|
||||
u64 := func(key string) uint64 {
|
||||
value, ok := row[key]
|
||||
if !ok || value == nil {
|
||||
return 0
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return uint64(typed)
|
||||
case string:
|
||||
number, _ := strconv.ParseUint(typed, 10, 64)
|
||||
return number
|
||||
case json.Number:
|
||||
number, _ := typed.Int64()
|
||||
return uint64(number)
|
||||
case int64:
|
||||
return uint64(typed)
|
||||
case uint64:
|
||||
return typed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
str := func(key string) string {
|
||||
value, ok := row[key]
|
||||
if !ok || value == nil {
|
||||
return ""
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
default:
|
||||
return fmt.Sprintf("%v", typed)
|
||||
}
|
||||
}
|
||||
|
||||
ts := func(key string) time.Time {
|
||||
value, ok := row[key]
|
||||
if !ok || value == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if typed == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if parsed, err := time.Parse(layout, typed); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
return time.Unix(int64(typed), 0)
|
||||
case json.Number:
|
||||
number, _ := typed.Int64()
|
||||
return time.Unix(number, 0)
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
boolValue := func(key string) bool {
|
||||
value, ok := row[key]
|
||||
if !ok || value == nil {
|
||||
return false
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed
|
||||
case float64:
|
||||
return typed > 0
|
||||
case int64:
|
||||
return typed > 0
|
||||
case uint64:
|
||||
return typed > 0
|
||||
case string:
|
||||
switch strings.ToLower(strings.TrimSpace(typed)) {
|
||||
case "1", "true", "yes":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
parseStringArray := func(key string) []string {
|
||||
value, ok := row[key]
|
||||
if !ok || value == nil {
|
||||
return nil
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case []string:
|
||||
return typed
|
||||
case []interface{}:
|
||||
result := make([]string, 0, len(typed))
|
||||
for _, one := range typed {
|
||||
if one == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, fmt.Sprintf("%v", one))
|
||||
}
|
||||
return result
|
||||
case string:
|
||||
if typed == "" {
|
||||
return nil
|
||||
}
|
||||
var result []string
|
||||
if json.Unmarshal([]byte(typed), &result) == nil {
|
||||
return result
|
||||
}
|
||||
return []string{typed}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
result.Timestamp = ts("timestamp")
|
||||
result.RequestId = str("request_id")
|
||||
result.NodeId = u64("node_id")
|
||||
result.ClusterId = u64("cluster_id")
|
||||
result.DomainId = u64("domain_id")
|
||||
result.RecordId = u64("record_id")
|
||||
result.RemoteAddr = str("remote_addr")
|
||||
result.QuestionName = str("question_name")
|
||||
result.QuestionType = str("question_type")
|
||||
result.RecordName = str("record_name")
|
||||
result.RecordType = str("record_type")
|
||||
result.RecordValue = str("record_value")
|
||||
result.Networking = str("networking")
|
||||
result.IsRecursive = boolValue("is_recursive")
|
||||
result.Error = str("error")
|
||||
result.NSRouteCodes = parseStringArray("ns_route_codes")
|
||||
result.ContentJSON = str("content_json")
|
||||
return result
|
||||
}
|
||||
|
||||
// NSRowToPB 将 ClickHouse 行转换为 pb.NSAccessLog.
|
||||
func NSRowToPB(row *NSLogsIngestRow) *pb.NSAccessLog {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log := &pb.NSAccessLog{
|
||||
NsNodeId: int64(row.NodeId),
|
||||
NsDomainId: int64(row.DomainId),
|
||||
NsRecordId: int64(row.RecordId),
|
||||
NsRouteCodes: row.NSRouteCodes,
|
||||
RemoteAddr: row.RemoteAddr,
|
||||
QuestionName: row.QuestionName,
|
||||
QuestionType: row.QuestionType,
|
||||
RecordName: row.RecordName,
|
||||
RecordType: row.RecordType,
|
||||
RecordValue: row.RecordValue,
|
||||
Networking: row.Networking,
|
||||
Timestamp: row.Timestamp.Unix(),
|
||||
RequestId: row.RequestId,
|
||||
Error: row.Error,
|
||||
IsRecursive: row.IsRecursive,
|
||||
}
|
||||
if !row.Timestamp.IsZero() {
|
||||
log.TimeLocal = row.Timestamp.Format("2/Jan/2006:15:04:05 -0700")
|
||||
}
|
||||
|
||||
if row.ContentJSON != "" {
|
||||
contentLog := &pb.NSAccessLog{}
|
||||
if json.Unmarshal([]byte(row.ContentJSON), contentLog) == nil {
|
||||
contentLog.RequestId = row.RequestId
|
||||
return contentLog
|
||||
}
|
||||
}
|
||||
|
||||
return log
|
||||
}
|
||||
198
EdgeAPI/internal/configs/api_config.go
Normal file
198
EdgeAPI/internal/configs/api_config.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var sharedAPIConfig *APIConfig = nil
|
||||
|
||||
// ClickHouseConfig 仅用于访问日志列表只读查询(logs_ingest)
|
||||
type ClickHouseConfig struct {
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port int `yaml:"port" json:"port"`
|
||||
User string `yaml:"user" json:"user"`
|
||||
Password string `yaml:"password" json:"password"`
|
||||
Database string `yaml:"database" json:"database"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
TLSSkipVerify bool `yaml:"tlsSkipVerify" json:"tlsSkipVerify"`
|
||||
TLSServerName string `yaml:"tlsServerName" json:"tlsServerName"`
|
||||
}
|
||||
|
||||
// APIConfig API节点配置
|
||||
type APIConfig struct {
|
||||
NodeId string `yaml:"nodeId" json:"nodeId"`
|
||||
Secret string `yaml:"secret" json:"secret"`
|
||||
ClickHouse *ClickHouseConfig `yaml:"clickhouse,omitempty" json:"clickhouse,omitempty"`
|
||||
|
||||
numberId int64 // 数字ID
|
||||
}
|
||||
|
||||
// SharedAPIConfig 获取共享配置
|
||||
func SharedAPIConfig() (*APIConfig, error) {
|
||||
sharedLocker.Lock()
|
||||
defer sharedLocker.Unlock()
|
||||
|
||||
if sharedAPIConfig != nil {
|
||||
return sharedAPIConfig, nil
|
||||
}
|
||||
|
||||
// 候选文件
|
||||
var config = &APIConfig{}
|
||||
{
|
||||
var localFile = Tea.ConfigFile("api.yaml")
|
||||
var isFromLocal = false
|
||||
var paths = []string{localFile}
|
||||
homeDir, homeErr := os.UserHomeDir()
|
||||
if homeErr == nil {
|
||||
paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/api.yaml")
|
||||
}
|
||||
paths = append(paths, "/etc/"+teaconst.ProcessName+"/api.yaml")
|
||||
|
||||
// 依次检查文件
|
||||
var data []byte
|
||||
var err error
|
||||
var firstErr error
|
||||
for _, path := range paths {
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
} else {
|
||||
if path == localFile {
|
||||
isFromLocal = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// 解析内容
|
||||
err = yaml.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isFromLocal {
|
||||
// 恢复文件
|
||||
_ = os.WriteFile(localFile, data, 0666)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复数据库文件
|
||||
{
|
||||
var dbConfigFile = Tea.ConfigFile("db.yaml")
|
||||
_, err := os.Stat(dbConfigFile)
|
||||
if err != nil {
|
||||
var paths = []string{}
|
||||
homeDir, homeErr := os.UserHomeDir()
|
||||
if homeErr == nil {
|
||||
paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/db.yaml")
|
||||
}
|
||||
paths = append(paths, "/etc/"+teaconst.ProcessName+"/db.yaml")
|
||||
for _, path := range paths {
|
||||
_, err = os.Stat(path)
|
||||
if err == nil {
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
_ = os.WriteFile(dbConfigFile, data, 0666)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedAPIConfig = config
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SetNumberId 设置数字ID
|
||||
func (this *APIConfig) SetNumberId(numberId int64) {
|
||||
this.numberId = numberId
|
||||
teaconst.NodeId = numberId
|
||||
}
|
||||
|
||||
// NumberId 获取数字ID
|
||||
func (this *APIConfig) NumberId() int64 {
|
||||
return this.numberId
|
||||
}
|
||||
|
||||
// WriteFile 保存到文件
|
||||
func (this *APIConfig) WriteFile(path string) error {
|
||||
data, err := yaml.Marshal(this)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成备份文件
|
||||
var filename = filepath.Base(path)
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
var backupDirs = []string{"/etc/edge-api"}
|
||||
if len(homeDir) > 0 {
|
||||
backupDirs = append(backupDirs, homeDir+"/.edge-api")
|
||||
}
|
||||
for _, backupDir := range backupDirs {
|
||||
stat, err := os.Stat(backupDir)
|
||||
if err == nil && stat.IsDir() {
|
||||
_ = os.WriteFile(backupDir+"/"+filename, data, 0666)
|
||||
} else if err != nil && os.IsNotExist(err) {
|
||||
err = os.Mkdir(backupDir, 0777)
|
||||
if err == nil {
|
||||
_ = os.WriteFile(backupDir+"/"+filename, data, 0666)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0666)
|
||||
}
|
||||
|
||||
// ResetAPIConfig 重置配置
|
||||
func ResetAPIConfig() error {
|
||||
for _, filename := range []string{"api.yaml", "db.yaml", ".db.yaml"} {
|
||||
// 重置 configs/api.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-api/api.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-api/api.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
|
||||
}
|
||||
14
EdgeAPI/internal/configs/api_config_test.go
Normal file
14
EdgeAPI/internal/configs/api_config_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSharedAPIConfig(t *testing.T) {
|
||||
config, err := SharedAPIConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(config)
|
||||
}
|
||||
26
EdgeAPI/internal/configs/db_config.go
Normal file
26
EdgeAPI/internal/configs/db_config.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func LoadDBConfig() (*dbs.Config, error) {
|
||||
var config = &dbs.Config{}
|
||||
for _, filename := range []string{".db.yaml", "db.yaml"} {
|
||||
configData, err := os.ReadFile(Tea.ConfigFile(filename))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
err = yaml.Unmarshal(configData, config)
|
||||
return config, err
|
||||
}
|
||||
|
||||
return nil, errors.New("could not find database config file '.db.yaml' or 'db.yaml'")
|
||||
}
|
||||
5
EdgeAPI/internal/configs/locker.go
Normal file
5
EdgeAPI/internal/configs/locker.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package configs
|
||||
|
||||
import "sync"
|
||||
|
||||
var sharedLocker = &sync.RWMutex{}
|
||||
60
EdgeAPI/internal/configs/simple_db_config.go
Normal file
60
EdgeAPI/internal/configs/simple_db_config.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
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 ParseSimpleDBConfig(data []byte) (*SimpleDBConfig, error) {
|
||||
var config = &SimpleDBConfig{}
|
||||
err := yaml.Unmarshal(data, config)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (this *SimpleDBConfig) GenerateOldConfig() 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{
|
||||
"dev": dbConfig,
|
||||
"prod": dbConfig,
|
||||
},
|
||||
}
|
||||
config.Default.DB = "prod"
|
||||
config.Fields = map[string][]string{
|
||||
"bool": this.BoolFields,
|
||||
}
|
||||
|
||||
oldConfigYAML, encodeErr := yaml.Marshal(config)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
|
||||
var targetFile = Tea.ConfigFile(".db.yaml")
|
||||
err := os.WriteFile(targetFile, oldConfigYAML, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create database config file failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
9
EdgeAPI/internal/const/build.go
Normal file
9
EdgeAPI/internal/const/build.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build !plus
|
||||
// +build !plus
|
||||
|
||||
package teaconst
|
||||
|
||||
const BuildCommunity = true
|
||||
const BuildPlus = false
|
||||
const Tag = "community"
|
||||
8
EdgeAPI/internal/const/build_plus.go
Normal file
8
EdgeAPI/internal/const/build_plus.go
Normal 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"
|
||||
22
EdgeAPI/internal/const/const.go
Normal file
22
EdgeAPI/internal/const/const.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.7" //1.3.9
|
||||
|
||||
ProductName = "Edge API"
|
||||
ProcessName = "edge-api"
|
||||
ProductNameZH = "Edge"
|
||||
|
||||
GlobalProductName = "GoEdge"
|
||||
|
||||
Role = "api"
|
||||
|
||||
EncryptMethod = "aes-256-cfb"
|
||||
|
||||
SystemdServiceName = "edge-api"
|
||||
|
||||
// 其他节点版本号,用来检测是否有需要升级的节点
|
||||
|
||||
NodeVersion = "1.4.7" //1.3.8.2
|
||||
)
|
||||
|
||||
9
EdgeAPI/internal/const/const_community.go
Normal file
9
EdgeAPI/internal/const/const_community.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build !plus
|
||||
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
// DefaultMaxNodes 节点数限制
|
||||
DefaultMaxNodes int32 = 50
|
||||
)
|
||||
13
EdgeAPI/internal/const/const_plus.go
Normal file
13
EdgeAPI/internal/const/const_plus.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
DNSNodeVersion = "1.4.7" //1.3.8.2
|
||||
UserNodeVersion = "1.4.7" //1.3.8.2
|
||||
ReportNodeVersion = "0.1.5"
|
||||
|
||||
DefaultMaxNodes int32 = 50
|
||||
)
|
||||
|
||||
35
EdgeAPI/internal/const/vars.go
Normal file
35
EdgeAPI/internal/const/vars.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package teaconst
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
IsPlus = false
|
||||
Edition = ""
|
||||
MaxNodes int32 = 0
|
||||
NodeId int64 = 0
|
||||
Debug = false
|
||||
InstanceCode = fmt.Sprintf("%x", sha1.Sum([]byte("INSTANCE"+types.String(time.Now().UnixNano())+"@"+types.String(rands.Int64()))))
|
||||
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, "___")
|
||||
}
|
||||
18
EdgeAPI/internal/const/vars_plus.go
Normal file
18
EdgeAPI/internal/const/vars_plus.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package teaconst
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
var AuthorityValidatorHOSTS = []string{}
|
||||
|
||||
func init() {
|
||||
if Tea.IsTesting() {
|
||||
AuthorityValidatorHOSTS = []string{"http://127.0.0.1:8084"} // local test
|
||||
} else {
|
||||
AuthorityValidatorHOSTS = []string{"https://authority.goedge.cn"}
|
||||
}
|
||||
}
|
||||
55
EdgeAPI/internal/db/db_test.go
Normal file
55
EdgeAPI/internal/db/db_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDB_Env(t *testing.T) {
|
||||
Tea.Env = "prod"
|
||||
t.Log(dbs.Default())
|
||||
}
|
||||
|
||||
func TestDB_Instance(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s")
|
||||
if err != nil {
|
||||
t.Fatal(i, "open:", err)
|
||||
}
|
||||
db.SetConnMaxIdleTime(time.Minute * 3)
|
||||
db.SetConnMaxLifetime(time.Minute * 3)
|
||||
db.SetMaxIdleConns(0)
|
||||
db.SetMaxOpenConns(1)
|
||||
go func(db *sql.DB, i int) {
|
||||
for j := 0; j < 100; j++ {
|
||||
err := db.Ping()
|
||||
if err != nil {
|
||||
if err == driver.ErrBadConn {
|
||||
return
|
||||
}
|
||||
t.Error(i, "exec:", err)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
t.Log(i, "ok", db)
|
||||
}(db, i)
|
||||
}
|
||||
time.Sleep(100 * time.Second)
|
||||
}
|
||||
|
||||
func TestDB_Reuse(t *testing.T) {
|
||||
var dao = models.NewVersionDAO()
|
||||
for i := 0; i < 20_000; i++ {
|
||||
_, _, err := dao.Query(nil).Attr("version", i).Reuse(true).FindOne()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
EdgeAPI/internal/db/models/accounts/order_method_dao.go
Normal file
33
EdgeAPI/internal/db/models/accounts/order_method_dao.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
const (
|
||||
OrderMethodStateEnabled = 1 // 已启用
|
||||
OrderMethodStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type OrderMethodDAO dbs.DAO
|
||||
|
||||
func NewOrderMethodDAO() *OrderMethodDAO {
|
||||
return dbs.NewDAO(&OrderMethodDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeOrderMethods",
|
||||
Model: new(OrderMethod),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*OrderMethodDAO)
|
||||
}
|
||||
|
||||
var SharedOrderMethodDAO *OrderMethodDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedOrderMethodDAO = NewOrderMethodDAO()
|
||||
})
|
||||
}
|
||||
180
EdgeAPI/internal/db/models/accounts/order_method_dao_plus.go
Normal file
180
EdgeAPI/internal/db/models/accounts/order_method_dao_plus.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
// EnableOrderMethod 启用条目
|
||||
func (this *OrderMethodDAO) EnableOrderMethod(tx *dbs.Tx, id uint32) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", OrderMethodStateEnabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableOrderMethod 禁用条目
|
||||
func (this *OrderMethodDAO) DisableOrderMethod(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", OrderMethodStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// FindEnabledOrderMethod 查找支付方式
|
||||
func (this *OrderMethodDAO) FindEnabledOrderMethod(tx *dbs.Tx, id int64) (*OrderMethod, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Attr("state", OrderMethodStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*OrderMethod), err
|
||||
}
|
||||
|
||||
// FindEnabledBasicOrderMethod 查找支付方式基本信息
|
||||
func (this *OrderMethodDAO) FindEnabledBasicOrderMethod(tx *dbs.Tx, id int64) (*OrderMethod, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Result("id", "code", "url", "isOn", "secret", "parentCode", "clientType").
|
||||
Attr("state", OrderMethodStateEnabled).
|
||||
Find()
|
||||
if err != nil || result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*OrderMethod), err
|
||||
}
|
||||
|
||||
// FindEnabledOrderMethodWithCode 根据代号查找支付方式
|
||||
func (this *OrderMethodDAO) FindEnabledOrderMethodWithCode(tx *dbs.Tx, code string) (*OrderMethod, error) {
|
||||
result, err := this.Query(tx).
|
||||
Attr("code", code).
|
||||
Attr("state", OrderMethodStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*OrderMethod), err
|
||||
}
|
||||
|
||||
// ExistOrderMethodWithCode 根据代号检查支付方式是否存在
|
||||
func (this *OrderMethodDAO) ExistOrderMethodWithCode(tx *dbs.Tx, code string, excludingMethodId int64) (bool, error) {
|
||||
var query = this.Query(tx)
|
||||
if excludingMethodId > 0 {
|
||||
query.Neq("id", excludingMethodId)
|
||||
}
|
||||
return query.
|
||||
Attr("code", code).
|
||||
Attr("state", OrderMethodStateEnabled).
|
||||
Exist()
|
||||
}
|
||||
|
||||
// FindOrderMethodName 根据主键查找名称
|
||||
func (this *OrderMethodDAO) FindOrderMethodName(tx *dbs.Tx, id int64) (string, error) {
|
||||
return this.Query(tx).
|
||||
Pk(id).
|
||||
Result("name").
|
||||
FindStringCol("")
|
||||
}
|
||||
|
||||
// CreateMethod 创建支付方式
|
||||
func (this *OrderMethodDAO) CreateMethod(tx *dbs.Tx, name string, code string, url string, description string, parentCode string, params any, clientType userconfigs.PayClientType, qrcodeTitle string) (int64, error) {
|
||||
var op = NewOrderMethodOperator()
|
||||
op.Name = name
|
||||
op.Code = code
|
||||
|
||||
op.Url = url
|
||||
|
||||
op.ParentCode = parentCode
|
||||
if params != nil {
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
op.Params = paramsJSON
|
||||
}
|
||||
|
||||
op.Description = description
|
||||
op.Secret = utils.Sha1RandomString()
|
||||
op.ClientType = clientType
|
||||
op.QrcodeTitle = qrcodeTitle
|
||||
op.IsOn = true
|
||||
op.State = OrderMethodStateEnabled
|
||||
return this.SaveInt64(tx, op)
|
||||
}
|
||||
|
||||
// UpdateMethod 修改支付方式
|
||||
// 切记不允许修改parentCode
|
||||
func (this *OrderMethodDAO) UpdateMethod(tx *dbs.Tx, methodId int64, name string, code string, url string, description string, params any, clientType userconfigs.PayClientType, qrcodeTitle string, isOn bool) error {
|
||||
if methodId <= 0 {
|
||||
return errors.New("invalid methodId")
|
||||
}
|
||||
|
||||
var op = NewOrderMethodOperator()
|
||||
op.Id = methodId
|
||||
op.Name = name
|
||||
op.Code = code
|
||||
op.Url = url
|
||||
op.Description = description
|
||||
|
||||
if params != nil {
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
op.Params = paramsJSON
|
||||
}
|
||||
|
||||
op.ClientType = clientType
|
||||
op.QrcodeTitle = qrcodeTitle
|
||||
|
||||
op.IsOn = isOn
|
||||
return this.Save(tx, op)
|
||||
}
|
||||
|
||||
// UpdateMethodOrders 修改排序
|
||||
func (this *OrderMethodDAO) UpdateMethodOrders(tx *dbs.Tx, methodIds []int64) error {
|
||||
var maxOrder = len(methodIds)
|
||||
for index, methodId := range methodIds {
|
||||
err := this.Query(tx).
|
||||
Pk(methodId).
|
||||
Set("order", maxOrder-index).
|
||||
UpdateQuickly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindAllEnabledMethodOrders 查找所有支付方式
|
||||
func (this *OrderMethodDAO) FindAllEnabledMethodOrders(tx *dbs.Tx) (result []*OrderMethod, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(OrderMethodStateEnabled).
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllEnabledAndOnMethodOrders 查找所有已启用的支付方式
|
||||
func (this *OrderMethodDAO) FindAllEnabledAndOnMethodOrders(tx *dbs.Tx) (result []*OrderMethod, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(OrderMethodStateEnabled).
|
||||
Attr("isOn", true).
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package accounts_test
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
40
EdgeAPI/internal/db/models/accounts/order_method_model.go
Normal file
40
EdgeAPI/internal/db/models/accounts/order_method_model.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package accounts
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// OrderMethod 订单支付方式
|
||||
type OrderMethod struct {
|
||||
Id uint32 `field:"id"` // ID
|
||||
Name string `field:"name"` // 名称
|
||||
IsOn bool `field:"isOn"` // 是否启用
|
||||
Description string `field:"description"` // 描述
|
||||
ParentCode string `field:"parentCode"` // 内置的父级代号
|
||||
Code string `field:"code"` // 代号
|
||||
Url string `field:"url"` // URL
|
||||
Secret string `field:"secret"` // 密钥
|
||||
Params dbs.JSON `field:"params"` // 参数
|
||||
ClientType string `field:"clientType"` // 客户端类型
|
||||
QrcodeTitle string `field:"qrcodeTitle"` // 二维码标题
|
||||
Order uint32 `field:"order"` // 排序
|
||||
State uint8 `field:"state"` // 状态
|
||||
}
|
||||
|
||||
type OrderMethodOperator struct {
|
||||
Id any // ID
|
||||
Name any // 名称
|
||||
IsOn any // 是否启用
|
||||
Description any // 描述
|
||||
ParentCode any // 内置的父级代号
|
||||
Code any // 代号
|
||||
Url any // URL
|
||||
Secret any // 密钥
|
||||
Params any // 参数
|
||||
ClientType any // 客户端类型
|
||||
QrcodeTitle any // 二维码标题
|
||||
Order any // 排序
|
||||
State any // 状态
|
||||
}
|
||||
|
||||
func NewOrderMethodOperator() *OrderMethodOperator {
|
||||
return &OrderMethodOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
@@ -0,0 +1,82 @@
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
)
|
||||
|
||||
type UserAccountDailyStatDAO dbs.DAO
|
||||
|
||||
func NewUserAccountDailyStatDAO() *UserAccountDailyStatDAO {
|
||||
return dbs.NewDAO(&UserAccountDailyStatDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeUserAccountDailyStats",
|
||||
Model: new(UserAccountDailyStat),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*UserAccountDailyStatDAO)
|
||||
}
|
||||
|
||||
var SharedUserAccountDailyStatDAO *UserAccountDailyStatDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedUserAccountDailyStatDAO = NewUserAccountDailyStatDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateDailyStat 更新当天统计数据
|
||||
func (this *UserAccountDailyStatDAO) UpdateDailyStat(tx *dbs.Tx) error {
|
||||
var day = timeutil.Format("Ymd")
|
||||
var month = timeutil.Format("Ym")
|
||||
income, err := SharedUserAccountLogDAO.SumDailyEventTypes(tx, day, userconfigs.AccountIncomeEventTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expense, err := SharedUserAccountLogDAO.SumDailyEventTypes(tx, day, userconfigs.AccountExpenseEventTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expense < 0 {
|
||||
expense = -expense
|
||||
}
|
||||
|
||||
return this.Query(tx).
|
||||
InsertOrUpdateQuickly(maps.Map{
|
||||
"day": day,
|
||||
"month": month,
|
||||
"income": income,
|
||||
"expense": expense,
|
||||
}, maps.Map{
|
||||
"income": income,
|
||||
"expense": expense,
|
||||
})
|
||||
}
|
||||
|
||||
// FindDailyStats 查看按天统计
|
||||
func (this *UserAccountDailyStatDAO) FindDailyStats(tx *dbs.Tx, dayFrom string, dayTo string) (result []*UserAccountDailyStat, err error) {
|
||||
_, err = this.Query(tx).
|
||||
Between("day", dayFrom, dayTo).
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindMonthlyStats 查看某月统计
|
||||
func (this *UserAccountDailyStatDAO) FindMonthlyStats(tx *dbs.Tx, dayFrom string, dayTo string) (result []*UserAccountDailyStat, err error) {
|
||||
_, err = this.Query(tx).
|
||||
Result("SUM(income) AS income", "SUM(expense) AS expense", "month").
|
||||
Between("day", dayFrom, dayTo).
|
||||
Group("month").
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package accounts
|
||||
|
||||
// UserAccountDailyStat 账户每日统计
|
||||
type UserAccountDailyStat struct {
|
||||
Id uint32 `field:"id"` // ID
|
||||
Day string `field:"day"` // YYYYMMDD
|
||||
Month string `field:"month"` // YYYYMM
|
||||
Income float64 `field:"income"` // 收入
|
||||
Expense float64 `field:"expense"` // 支出
|
||||
}
|
||||
|
||||
type UserAccountDailyStatOperator struct {
|
||||
Id interface{} // ID
|
||||
Day interface{} // YYYYMMDD
|
||||
Month interface{} // YYYYMM
|
||||
Income interface{} // 收入
|
||||
Expense interface{} // 支出
|
||||
}
|
||||
|
||||
func NewUserAccountDailyStatOperator() *UserAccountDailyStatOperator {
|
||||
return &UserAccountDailyStatOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
264
EdgeAPI/internal/db/models/accounts/user_account_dao_plus.go
Normal file
264
EdgeAPI/internal/db/models/accounts/user_account_dao_plus.go
Normal file
@@ -0,0 +1,264 @@
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dbs.OnReadyDone(func() {
|
||||
goman.New(func() {
|
||||
// 自动支付账单任务
|
||||
var ticker = time.NewTicker(6 * time.Hour)
|
||||
for range ticker.C {
|
||||
if SharedUserAccountDAO.Instance != nil {
|
||||
err := SharedUserAccountDAO.Instance.RunTx(func(tx *dbs.Tx) error {
|
||||
return SharedUserAccountDAO.PayBillsAutomatically(tx)
|
||||
})
|
||||
if err != nil {
|
||||
remotelogs.Error("USER_ACCOUNT_DAO", "pay bills task failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type UserAccountDAO dbs.DAO
|
||||
|
||||
func NewUserAccountDAO() *UserAccountDAO {
|
||||
return dbs.NewDAO(&UserAccountDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeUserAccounts",
|
||||
Model: new(UserAccount),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*UserAccountDAO)
|
||||
}
|
||||
|
||||
var SharedUserAccountDAO *UserAccountDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedUserAccountDAO = NewUserAccountDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// FindUserAccountWithUserId 根据用户ID查找用户账户
|
||||
func (this *UserAccountDAO) FindUserAccountWithUserId(tx *dbs.Tx, userId int64) (*UserAccount, error) {
|
||||
if userId <= 0 {
|
||||
return nil, errors.New("invalid userId '" + types.String(userId) + "'")
|
||||
}
|
||||
|
||||
// 用户是否存在
|
||||
user, err := models.SharedUserDAO.FindEnabledUser(tx, userId, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
account, err := this.Query(tx).
|
||||
Attr("userId", userId).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if account != nil {
|
||||
return account.(*UserAccount), nil
|
||||
}
|
||||
|
||||
var op = NewUserAccountOperator()
|
||||
op.UserId = userId
|
||||
_, err = this.SaveInt64(tx, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return this.FindUserAccountWithUserId(tx, userId)
|
||||
}
|
||||
|
||||
// FindUserAccountWithAccountId 根据ID查找用户账户
|
||||
func (this *UserAccountDAO) FindUserAccountWithAccountId(tx *dbs.Tx, accountId int64) (*UserAccount, error) {
|
||||
one, err := this.Query(tx).
|
||||
Pk(accountId).
|
||||
Find()
|
||||
if one != nil {
|
||||
return one.(*UserAccount), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// UpdateUserAccount 操作用户账户
|
||||
func (this *UserAccountDAO) UpdateUserAccount(tx *dbs.Tx, accountId int64, delta float64, eventType userconfigs.AccountEventType, description string, params maps.Map) error {
|
||||
// 截取两位两位小数
|
||||
var realDelta = delta
|
||||
if realDelta < 0 {
|
||||
realDelta = numberutils.FloorFloat64(realDelta, 2)
|
||||
}
|
||||
|
||||
account, err := this.FindUserAccountWithAccountId(tx, accountId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil {
|
||||
return errors.New("invalid account id '" + types.String(accountId) + "'")
|
||||
}
|
||||
var userId = int64(account.UserId)
|
||||
if delta < 0 && account.Total < -delta {
|
||||
return errors.New("not enough account quota to decrease")
|
||||
}
|
||||
|
||||
// 操作账户
|
||||
err = this.Query(tx).
|
||||
Pk(account.Id).
|
||||
Set("total", dbs.SQL("total+:delta")).
|
||||
Param("delta", realDelta).
|
||||
UpdateQuickly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成日志
|
||||
err = SharedUserAccountLogDAO.CreateAccountLog(tx, userId, accountId, realDelta, 0, eventType, description, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserAccountFrozen 操作用户账户冻结余额
|
||||
func (this *UserAccountDAO) UpdateUserAccountFrozen(tx *dbs.Tx, userId int64, delta float64, eventType userconfigs.AccountEventType, description string, params maps.Map) error {
|
||||
account, err := this.FindUserAccountWithUserId(tx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil {
|
||||
return errors.New("invalid user account")
|
||||
}
|
||||
var deltaFloat64 = delta
|
||||
if deltaFloat64 < 0 && account.TotalFrozen < -deltaFloat64 {
|
||||
return errors.New("not enough account frozen quota to decrease")
|
||||
}
|
||||
|
||||
// 操作账户
|
||||
err = this.Query(tx).
|
||||
Pk(account.Id).
|
||||
Set("totalFrozen", dbs.SQL("total+:delta")).
|
||||
Param("delta", delta).
|
||||
UpdateQuickly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成日志
|
||||
err = SharedUserAccountLogDAO.CreateAccountLog(tx, userId, int64(account.Id), 0, delta, eventType, description, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountAllAccounts 计算所有账户数量
|
||||
func (this *UserAccountDAO) CountAllAccounts(tx *dbs.Tx, keyword string) (int64, error) {
|
||||
var query = this.Query(tx)
|
||||
if len(keyword) > 0 {
|
||||
query.Where("userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1 AND (username LIKE :keyword OR fullname LIKE :keyword))")
|
||||
query.Param("keyword", keyword)
|
||||
} else {
|
||||
query.Where("userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1)")
|
||||
}
|
||||
return query.Count()
|
||||
}
|
||||
|
||||
// ListAccounts 列出单页账户
|
||||
func (this *UserAccountDAO) ListAccounts(tx *dbs.Tx, keyword string, offset int64, size int64) (result []*UserAccount, err error) {
|
||||
var query = this.Query(tx)
|
||||
if len(keyword) > 0 {
|
||||
query.Where("userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1 AND (username LIKE :keyword OR fullname LIKE :keyword))")
|
||||
query.Param("keyword", keyword)
|
||||
} else {
|
||||
query.Where("userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1)")
|
||||
}
|
||||
_, err = query.
|
||||
DescPk().
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// PayBills 尝试自动支付账单
|
||||
func (this *UserAccountDAO) PayBillsAutomatically(tx *dbs.Tx) error {
|
||||
bills, err := models.SharedUserBillDAO.FindUnpaidBills(tx, 10000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 先支付久远的
|
||||
lists.Reverse(bills)
|
||||
|
||||
for _, bill := range bills {
|
||||
if bill.Amount <= 0 {
|
||||
err = models.SharedUserBillDAO.UpdateUserBillIsPaid(tx, int64(bill.Id), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
account, err := SharedUserAccountDAO.FindUserAccountWithUserId(tx, int64(bill.UserId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil || account.Total < bill.Amount {
|
||||
continue
|
||||
}
|
||||
|
||||
// 扣款
|
||||
err = SharedUserAccountDAO.UpdateUserAccount(tx, int64(account.Id), -bill.Amount, userconfigs.AccountEventTypePayBill, "支付账单"+bill.Code, maps.Map{"billId": bill.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 改为已支付
|
||||
err = models.SharedUserBillDAO.UpdateUserBillIsPaid(tx, int64(bill.Id), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckUserAccount 检查用户账户
|
||||
func (this *UserAccountDAO) CheckUserAccount(tx *dbs.Tx, userId int64, accountId int64) error {
|
||||
exists, err := this.Query(tx).
|
||||
Pk(accountId).
|
||||
Attr("userId", userId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return models.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//go:build plus
|
||||
|
||||
package accounts_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/accounts"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserAccountDAO_PayBills(t *testing.T) {
|
||||
dbs.NotifyReady()
|
||||
|
||||
err := accounts.NewUserAccountDAO().PayBillsAutomatically(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
|
||||
func TestUserAccountDAO_UpdateUserAccount(t *testing.T) {
|
||||
dbs.NotifyReady()
|
||||
var tx *dbs.Tx
|
||||
var dao = accounts.NewUserAccountDAO()
|
||||
|
||||
err := dao.UpdateUserAccount(tx, 3, -17.88, userconfigs.AccountEventTypePayBill, "test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
|
||||
// 221745.12
|
||||
func TestUserAccountDAO_UpdateUserAccount2(t *testing.T) {
|
||||
dbs.NotifyReady()
|
||||
var tx *dbs.Tx
|
||||
var dao = accounts.NewUserAccountDAO()
|
||||
|
||||
err := dao.UpdateUserAccount(tx, 3, -221745.12, userconfigs.AccountEventTypePayBill, "test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
131
EdgeAPI/internal/db/models/accounts/user_account_log_dao_plus.go
Normal file
131
EdgeAPI/internal/db/models/accounts/user_account_log_dao_plus.go
Normal file
@@ -0,0 +1,131 @@
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
dbutils "github.com/TeaOSLab/EdgeAPI/internal/db/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
)
|
||||
|
||||
type UserAccountLogDAO dbs.DAO
|
||||
|
||||
func NewUserAccountLogDAO() *UserAccountLogDAO {
|
||||
return dbs.NewDAO(&UserAccountLogDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeUserAccountLogs",
|
||||
Model: new(UserAccountLog),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*UserAccountLogDAO)
|
||||
}
|
||||
|
||||
var SharedUserAccountLogDAO *UserAccountLogDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedUserAccountLogDAO = NewUserAccountLogDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAccountLog 生成用户账户日志
|
||||
func (this *UserAccountLogDAO) CreateAccountLog(tx *dbs.Tx, userId int64, accountId int64, delta float64, deltaFrozen float64, eventType userconfigs.AccountEventType, description string, params maps.Map) error {
|
||||
var op = NewUserAccountLogOperator()
|
||||
op.UserId = userId
|
||||
op.AccountId = accountId
|
||||
op.Delta = delta
|
||||
op.DeltaFrozen = deltaFrozen
|
||||
|
||||
account, err := SharedUserAccountDAO.FindUserAccountWithAccountId(tx, accountId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil {
|
||||
return errors.New("invalid account id '" + types.String(accountId) + "'")
|
||||
}
|
||||
op.Total = account.Total
|
||||
op.TotalFrozen = account.TotalFrozen
|
||||
|
||||
op.EventType = eventType
|
||||
op.Description = description
|
||||
|
||||
if params == nil {
|
||||
params = maps.Map{}
|
||||
}
|
||||
op.Params = params.AsJSON()
|
||||
|
||||
op.Day = timeutil.Format("Ymd")
|
||||
err = this.Save(tx, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SharedUserAccountDailyStatDAO.UpdateDailyStat(tx)
|
||||
}
|
||||
|
||||
// CountAccountLogs 计算日志数量
|
||||
func (this *UserAccountLogDAO) CountAccountLogs(tx *dbs.Tx, userId int64, accountId int64, keyword string, eventType string) (int64, error) {
|
||||
var query = this.Query(tx)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
if accountId > 0 {
|
||||
query.Attr("accountId", accountId)
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1 AND (username LIKE :keyword OR fullname LIKE :keyword)) OR description LIKE :keyword)")
|
||||
query.Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
if len(eventType) > 0 {
|
||||
query.Attr("eventType", eventType)
|
||||
}
|
||||
return query.Count()
|
||||
}
|
||||
|
||||
// ListAccountLogs 列出单页日志
|
||||
func (this *UserAccountLogDAO) ListAccountLogs(tx *dbs.Tx, userId int64, accountId int64, keyword string, eventType string, offset int64, size int64) (result []*UserAccountLog, err error) {
|
||||
var query = this.Query(tx)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
if accountId > 0 {
|
||||
query.Attr("accountId", accountId)
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(userId IN (SELECT id FROM " + models.SharedUserDAO.Table + " WHERE state=1 AND (username LIKE :keyword OR fullname LIKE :keyword)) OR description LIKE :keyword)")
|
||||
query.Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
if len(eventType) > 0 {
|
||||
query.Attr("eventType", eventType)
|
||||
}
|
||||
_, err = query.
|
||||
DescPk().
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// SumDailyEventTypes 统计某天数据总和
|
||||
func (this *UserAccountLogDAO) SumDailyEventTypes(tx *dbs.Tx, day string, eventTypes []userconfigs.AccountEventType) (float32, error) {
|
||||
if len(eventTypes) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
result, err := this.Query(tx).
|
||||
Attr("day", day).
|
||||
Attr("eventType", eventTypes).
|
||||
Sum("delta", 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return types.Float32(result), nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package accounts
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// UserAccountLog 用户账户日志
|
||||
type UserAccountLog struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
UserId uint64 `field:"userId"` // 用户ID
|
||||
AccountId uint64 `field:"accountId"` // 账户ID
|
||||
Delta float64 `field:"delta"` // 操作余额的数量(可为负)
|
||||
DeltaFrozen float64 `field:"deltaFrozen"` // 操作冻结的数量(可为负)
|
||||
Total float64 `field:"total"` // 操作后余额
|
||||
TotalFrozen float64 `field:"totalFrozen"` // 操作后冻结余额
|
||||
EventType string `field:"eventType"` // 类型
|
||||
Description string `field:"description"` // 描述文字
|
||||
Day string `field:"day"` // YYYYMMDD
|
||||
CreatedAt uint64 `field:"createdAt"` // 时间
|
||||
Params dbs.JSON `field:"params"` // 参数
|
||||
}
|
||||
|
||||
type UserAccountLogOperator struct {
|
||||
Id interface{} // ID
|
||||
UserId interface{} // 用户ID
|
||||
AccountId interface{} // 账户ID
|
||||
Delta interface{} // 操作余额的数量(可为负)
|
||||
DeltaFrozen interface{} // 操作冻结的数量(可为负)
|
||||
Total interface{} // 操作后余额
|
||||
TotalFrozen interface{} // 操作后冻结余额
|
||||
EventType interface{} // 类型
|
||||
Description interface{} // 描述文字
|
||||
Day interface{} // YYYYMMDD
|
||||
CreatedAt interface{} // 时间
|
||||
Params interface{} // 参数
|
||||
}
|
||||
|
||||
func NewUserAccountLogOperator() *UserAccountLogOperator {
|
||||
return &UserAccountLogOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
20
EdgeAPI/internal/db/models/accounts/user_account_model.go
Normal file
20
EdgeAPI/internal/db/models/accounts/user_account_model.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package accounts
|
||||
|
||||
// UserAccount 用户账号
|
||||
type UserAccount struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
UserId uint64 `field:"userId"` // 用户ID
|
||||
Total float64 `field:"total"` // 可用总余额
|
||||
TotalFrozen float64 `field:"totalFrozen"` // 冻结余额
|
||||
}
|
||||
|
||||
type UserAccountOperator struct {
|
||||
Id interface{} // ID
|
||||
UserId interface{} // 用户ID
|
||||
Total interface{} // 可用总余额
|
||||
TotalFrozen interface{} // 冻结余额
|
||||
}
|
||||
|
||||
func NewUserAccountOperator() *UserAccountOperator {
|
||||
return &UserAccountOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
33
EdgeAPI/internal/db/models/accounts/user_order_dao.go
Normal file
33
EdgeAPI/internal/db/models/accounts/user_order_dao.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
const (
|
||||
UserOrderStateEnabled = 1 // 已启用
|
||||
UserOrderStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type UserOrderDAO dbs.DAO
|
||||
|
||||
func NewUserOrderDAO() *UserOrderDAO {
|
||||
return dbs.NewDAO(&UserOrderDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeUserOrders",
|
||||
Model: new(UserOrder),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*UserOrderDAO)
|
||||
}
|
||||
|
||||
var SharedUserOrderDAO *UserOrderDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedUserOrderDAO = NewUserOrderDAO()
|
||||
})
|
||||
}
|
||||
292
EdgeAPI/internal/db/models/accounts/user_order_dao_plus.go
Normal file
292
EdgeAPI/internal/db/models/accounts/user_order_dao_plus.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/nameservers"
|
||||
dbutils "github.com/TeaOSLab/EdgeAPI/internal/db/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateOrder 创建订单
|
||||
func (this *UserOrderDAO) CreateOrder(tx *dbs.Tx, adminId int64, userId int64, orderType userconfigs.OrderType, methodId int64, amount float64, paramsJSON []byte) (orderId int64, code string, err error) {
|
||||
// 查询过期时间
|
||||
configJSON, err := models.SharedSysSettingDAO.ReadSetting(tx, systemconfigs.SettingCodeUserOrderConfig)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
var config = userconfigs.DefaultUserOrderConfig()
|
||||
if len(configJSON) == 0 {
|
||||
err = json.Unmarshal(configJSON, config)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("decode order config failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存订单
|
||||
var op = NewUserOrderOperator()
|
||||
op.UserId = userId
|
||||
op.Type = orderType
|
||||
op.MethodId = methodId
|
||||
op.Amount = amount
|
||||
if len(paramsJSON) > 0 {
|
||||
op.Params = paramsJSON
|
||||
}
|
||||
op.Status = userconfigs.OrderStatusNone
|
||||
|
||||
if config.OrderLife != nil && config.OrderLife.Count > 0 {
|
||||
op.ExpiredAt = time.Now().Unix() + int64(config.OrderLife.Duration().Seconds())
|
||||
} else {
|
||||
op.ExpiredAt = time.Now().Unix() + 3600 /** 默认一个小时 **/
|
||||
}
|
||||
|
||||
op.State = UserOrderStateEnabled
|
||||
orderId, err = this.SaveInt64(tx, op)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
var orderCode = timeutil.Format("Ymd") + fmt.Sprintf("%08d", orderId) // 16 bytes
|
||||
err = this.Query(tx).
|
||||
Pk(orderId).
|
||||
Set("code", orderCode).
|
||||
UpdateQuickly()
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
// 生成订单日志
|
||||
err = SharedUserOrderLogDAO.CreateOrderLog(tx, adminId, userId, orderId, userconfigs.OrderStatusNone)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return orderId, orderCode, nil
|
||||
}
|
||||
|
||||
// FindEnabledOrder 查找某个订单
|
||||
func (this *UserOrderDAO) FindEnabledOrder(tx *dbs.Tx, orderId int64) (*UserOrder, error) {
|
||||
one, err := this.Query(tx).
|
||||
Pk(orderId).
|
||||
Find()
|
||||
if err != nil || one == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return one.(*UserOrder), nil
|
||||
}
|
||||
|
||||
// CancelOrder 取消订单
|
||||
func (this *UserOrderDAO) CancelOrder(tx *dbs.Tx, adminId int64, userId int64, orderId int64) error {
|
||||
status, err := this.Query(tx).
|
||||
Pk(orderId).
|
||||
Result("status").
|
||||
FindStringCol("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != userconfigs.OrderStatusNone {
|
||||
return errors.New("can not cancel the order with status '" + status + "'")
|
||||
}
|
||||
|
||||
err = this.Query(tx).
|
||||
Pk(orderId).
|
||||
Set("status", userconfigs.OrderStatusCancelled).
|
||||
Set("cancelledAt", time.Now().Unix()).
|
||||
UpdateQuickly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SharedUserOrderLogDAO.CreateOrderLog(tx, adminId, userId, orderId, userconfigs.OrderStatusCancelled)
|
||||
}
|
||||
|
||||
// FinishOrder 完成订单
|
||||
// 不需要检查过期时间,因为用户可能在支付页面停留非常久后才完成支付
|
||||
func (this *UserOrderDAO) FinishOrder(tx *dbs.Tx, adminId int64, userId int64, orderId int64) error {
|
||||
// 检查订单状态
|
||||
order, err := SharedUserOrderDAO.FindEnabledOrder(tx, orderId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if order == nil {
|
||||
return errors.New("can not find order")
|
||||
}
|
||||
|
||||
if order.Status != userconfigs.OrderStatusNone {
|
||||
return errors.New("you can not finish the order, cause order status is '" + order.Status + "'")
|
||||
}
|
||||
|
||||
// 用户账户
|
||||
account, err := SharedUserAccountDAO.FindUserAccountWithUserId(tx, int64(order.UserId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account == nil {
|
||||
return errors.New("user account not found")
|
||||
}
|
||||
|
||||
switch order.Type {
|
||||
case userconfigs.OrderTypeCharge: // 充值
|
||||
err = SharedUserAccountDAO.UpdateUserAccount(tx, int64(account.Id), order.Amount, userconfigs.AccountEventTypeCharge, "充值,订单号:"+order.Code, maps.Map{
|
||||
"orderCode": order.Code,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case userconfigs.OrderTypeBuyNSPlan: // 购买DNS套餐
|
||||
var params = &userconfigs.OrderTypeBuyNSPlanParams{}
|
||||
err = json.Unmarshal(order.Params, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = nameservers.SharedNSUserPlanDAO.RenewUserPlan(tx, int64(order.UserId), params.PlanId, params.DayFrom, params.DayTo, params.Period)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case userconfigs.OrderTypeBuyTrafficPackage: // 购买流量包
|
||||
var params = &userconfigs.OrderTypeBuyTrafficPackageParams{}
|
||||
err = json.Unmarshal(order.Params, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if params.Count > 0 {
|
||||
for i := 0; i < int(params.Count); i++ {
|
||||
_, err = models.SharedUserTrafficPackageDAO.CreateUserPackage(tx, int64(order.UserId), 0, params.PackageId, params.RegionId, params.PeriodId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case userconfigs.OrderTypeBuyAntiDDoSInstance: // 购买高防实例
|
||||
var params = &userconfigs.OrderTypeBuyAntiDDoSInstanceParams{}
|
||||
err = json.Unmarshal(order.Params, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = models.SharedUserADInstanceDAO.CreateUserADInstances(tx, int64(order.UserId), params.PackageId, params.PeriodId, params.Count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case userconfigs.OrderTypeRenewAntiDDoSInstance: // 续费高防实例
|
||||
var params = &userconfigs.OrderTypeRenewAntiDDoSInstanceParams{}
|
||||
err = json.Unmarshal(order.Params, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userInstance, err := models.SharedUserADInstanceDAO.FindEnabledUserADInstance(tx, params.UserInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if userInstance == nil {
|
||||
return errors.New("could not find user instance '" + types.String(params.UserInstanceId) + "'")
|
||||
}
|
||||
|
||||
period, err := models.SharedADPackagePeriodDAO.FindEnabledADPackagePeriod(tx, params.PeriodId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if period == nil {
|
||||
return errors.New("could not find period '" + types.String(params.PeriodId) + "'")
|
||||
}
|
||||
|
||||
_, err = models.SharedUserADInstanceDAO.RenewUserInstance(tx, userInstance, period)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = this.Query(tx).
|
||||
Pk(orderId).
|
||||
Set("status", userconfigs.OrderStatusFinished).
|
||||
Set("finishedAt", time.Now().Unix()).
|
||||
UpdateQuickly()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SharedUserOrderLogDAO.CreateOrderLog(tx, adminId, userId, orderId, userconfigs.OrderStatusFinished)
|
||||
}
|
||||
|
||||
// CountEnabledUserOrders 计算订单数量
|
||||
func (this *UserOrderDAO) CountEnabledUserOrders(tx *dbs.Tx, userId int64, status userconfigs.OrderStatus, keyword string) (int64, error) {
|
||||
var query = this.Query(tx).
|
||||
State(UserOrderStateEnabled)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
if len(status) > 0 {
|
||||
query.Attr("status", status)
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(code LIKE :keyword)")
|
||||
query.Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
return query.Count()
|
||||
}
|
||||
|
||||
// ListEnabledUserOrders 列出单页订单
|
||||
func (this *UserOrderDAO) ListEnabledUserOrders(tx *dbs.Tx, userId int64, status userconfigs.OrderStatus, keyword string, offset int64, size int64) (result []*UserOrder, err error) {
|
||||
var query = this.Query(tx).
|
||||
State(UserOrderStateEnabled)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
if len(status) > 0 {
|
||||
query.Attr("status", status)
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(code LIKE :keyword)")
|
||||
query.Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
_, err = query.
|
||||
DescPk().
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindUserOrderIdWithCode 根据订单号查找订单ID
|
||||
func (this *UserOrderDAO) FindUserOrderIdWithCode(tx *dbs.Tx, code string) (int64, error) {
|
||||
if len(code) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return this.Query(tx).
|
||||
ResultPk().
|
||||
State(UserOrderStateEnabled).
|
||||
Attr("code", code).
|
||||
FindInt64Col(0)
|
||||
}
|
||||
|
||||
// FindUserOrderWithCode 根据订单号查找订单
|
||||
func (this *UserOrderDAO) FindUserOrderWithCode(tx *dbs.Tx, code string) (*UserOrder, error) {
|
||||
if len(code) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
one, err := this.Query(tx).
|
||||
State(UserOrderStateEnabled).
|
||||
Attr("code", code).
|
||||
Find()
|
||||
if err != nil || one == nil {
|
||||
return nil, err
|
||||
}
|
||||
return one.(*UserOrder), nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package accounts_test
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
28
EdgeAPI/internal/db/models/accounts/user_order_log_dao.go
Normal file
28
EdgeAPI/internal/db/models/accounts/user_order_log_dao.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
type UserOrderLogDAO dbs.DAO
|
||||
|
||||
func NewUserOrderLogDAO() *UserOrderLogDAO {
|
||||
return dbs.NewDAO(&UserOrderLogDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeUserOrderLogs",
|
||||
Model: new(UserOrderLog),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*UserOrderLogDAO)
|
||||
}
|
||||
|
||||
var SharedUserOrderLogDAO *UserOrderLogDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedUserOrderLogDAO = NewUserOrderLogDAO()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateOrderLog 创建订单日志
|
||||
func (this *UserOrderLogDAO) CreateOrderLog(tx *dbs.Tx, adminId int64, userId int64, orderId int64, status userconfigs.OrderStatus) error {
|
||||
var op = NewUserOrderLogOperator()
|
||||
op.AdminId = adminId
|
||||
op.UserId = userId
|
||||
op.OrderId = orderId
|
||||
op.Status = status
|
||||
op.CreatedAt = time.Now().Unix()
|
||||
return this.Save(tx, op)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package accounts_test
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
28
EdgeAPI/internal/db/models/accounts/user_order_log_model.go
Normal file
28
EdgeAPI/internal/db/models/accounts/user_order_log_model.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package accounts
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// UserOrderLog 订单日志
|
||||
type UserOrderLog struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
AdminId uint64 `field:"adminId"` // 管理员ID
|
||||
UserId uint64 `field:"userId"` // 用户ID
|
||||
OrderId uint64 `field:"orderId"` // 订单ID
|
||||
Status string `field:"status"` // 状态
|
||||
Snapshot dbs.JSON `field:"snapshot"` // 状态快照
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
type UserOrderLogOperator struct {
|
||||
Id interface{} // ID
|
||||
AdminId interface{} // 管理员ID
|
||||
UserId interface{} // 用户ID
|
||||
OrderId interface{} // 订单ID
|
||||
Status interface{} // 状态
|
||||
Snapshot interface{} // 状态快照
|
||||
CreatedAt interface{} // 创建时间
|
||||
}
|
||||
|
||||
func NewUserOrderLogOperator() *UserOrderLogOperator {
|
||||
return &UserOrderLogOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
40
EdgeAPI/internal/db/models/accounts/user_order_model.go
Normal file
40
EdgeAPI/internal/db/models/accounts/user_order_model.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package accounts
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// UserOrder 用户订单
|
||||
type UserOrder struct {
|
||||
Id uint64 `field:"id"` // 用户订单
|
||||
UserId uint64 `field:"userId"` // 用户ID
|
||||
Code string `field:"code"` // 订单号
|
||||
Type string `field:"type"` // 订单类型
|
||||
MethodId uint32 `field:"methodId"` // 支付方式
|
||||
Status string `field:"status"` // 订单状态
|
||||
Amount float64 `field:"amount"` // 金额
|
||||
Params dbs.JSON `field:"params"` // 附加参数
|
||||
ExpiredAt uint64 `field:"expiredAt"` // 过期时间
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
CancelledAt uint64 `field:"cancelledAt"` // 取消时间
|
||||
FinishedAt uint64 `field:"finishedAt"` // 结束时间
|
||||
State uint8 `field:"state"` // 状态
|
||||
}
|
||||
|
||||
type UserOrderOperator struct {
|
||||
Id interface{} // 用户订单
|
||||
UserId interface{} // 用户ID
|
||||
Code interface{} // 订单号
|
||||
Type interface{} // 订单类型
|
||||
MethodId interface{} // 支付方式
|
||||
Status interface{} // 订单状态
|
||||
Amount interface{} // 金额
|
||||
Params interface{} // 附加参数
|
||||
ExpiredAt interface{} // 过期时间
|
||||
CreatedAt interface{} // 创建时间
|
||||
CancelledAt interface{} // 取消时间
|
||||
FinishedAt interface{} // 结束时间
|
||||
State interface{} // 状态
|
||||
}
|
||||
|
||||
func NewUserOrderOperator() *UserOrderOperator {
|
||||
return &UserOrderOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package accounts
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build plus
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IsExpired 检查当前订单是否已经过期
|
||||
func (this *UserOrder) IsExpired() bool {
|
||||
return this.Status == userconfigs.OrderStatusNone &&
|
||||
time.Now().Unix() > int64(this.ExpiredAt)
|
||||
}
|
||||
54
EdgeAPI/internal/db/models/acme/acme_authentication_dao.go
Normal file
54
EdgeAPI/internal/db/models/acme/acme_authentication_dao.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
type ACMEAuthenticationDAO dbs.DAO
|
||||
|
||||
func NewACMEAuthenticationDAO() *ACMEAuthenticationDAO {
|
||||
return dbs.NewDAO(&ACMEAuthenticationDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeACMEAuthentications",
|
||||
Model: new(ACMEAuthentication),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ACMEAuthenticationDAO)
|
||||
}
|
||||
|
||||
var SharedACMEAuthenticationDAO *ACMEAuthenticationDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedACMEAuthenticationDAO = NewACMEAuthenticationDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// 创建认证信息
|
||||
func (this *ACMEAuthenticationDAO) CreateAuth(tx *dbs.Tx, taskId int64, domain string, token string, key string) error {
|
||||
var op = NewACMEAuthenticationOperator()
|
||||
op.TaskId = taskId
|
||||
op.Domain = domain
|
||||
op.Token = token
|
||||
op.Key = key
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// 根据令牌查找认证信息
|
||||
func (this *ACMEAuthenticationDAO) FindAuthWithToken(tx *dbs.Tx, token string) (*ACMEAuthentication, error) {
|
||||
one, err := this.Query(tx).
|
||||
Attr("token", token).
|
||||
DescPk().
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if one == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return one.(*ACMEAuthentication), nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
24
EdgeAPI/internal/db/models/acme/acme_authentication_model.go
Normal file
24
EdgeAPI/internal/db/models/acme/acme_authentication_model.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package acme
|
||||
|
||||
// ACME认证
|
||||
type ACMEAuthentication struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
TaskId uint64 `field:"taskId"` // 任务ID
|
||||
Domain string `field:"domain"` // 域名
|
||||
Token string `field:"token"` // 令牌
|
||||
Key string `field:"key"` // 密钥
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
type ACMEAuthenticationOperator struct {
|
||||
Id interface{} // ID
|
||||
TaskId interface{} // 任务ID
|
||||
Domain interface{} // 域名
|
||||
Token interface{} // 令牌
|
||||
Key interface{} // 密钥
|
||||
CreatedAt interface{} // 创建时间
|
||||
}
|
||||
|
||||
func NewACMEAuthenticationOperator() *ACMEAuthenticationOperator {
|
||||
return &ACMEAuthenticationOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package acme
|
||||
154
EdgeAPI/internal/db/models/acme/acme_provider_account_dao.go
Normal file
154
EdgeAPI/internal/db/models/acme/acme_provider_account_dao.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
const (
|
||||
ACMEProviderAccountStateEnabled = 1 // 已启用
|
||||
ACMEProviderAccountStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type ACMEProviderAccountDAO dbs.DAO
|
||||
|
||||
func NewACMEProviderAccountDAO() *ACMEProviderAccountDAO {
|
||||
return dbs.NewDAO(&ACMEProviderAccountDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeACMEProviderAccounts",
|
||||
Model: new(ACMEProviderAccount),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ACMEProviderAccountDAO)
|
||||
}
|
||||
|
||||
var SharedACMEProviderAccountDAO *ACMEProviderAccountDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedACMEProviderAccountDAO = NewACMEProviderAccountDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// EnableACMEProviderAccount 启用条目
|
||||
func (this *ACMEProviderAccountDAO) EnableACMEProviderAccount(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMEProviderAccountStateEnabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableACMEProviderAccount 禁用条目
|
||||
func (this *ACMEProviderAccountDAO) DisableACMEProviderAccount(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMEProviderAccountStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// FindEnabledACMEProviderAccount 查找启用中的条目
|
||||
func (this *ACMEProviderAccountDAO) FindEnabledACMEProviderAccount(tx *dbs.Tx, id int64) (*ACMEProviderAccount, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Attr("state", ACMEProviderAccountStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*ACMEProviderAccount), err
|
||||
}
|
||||
|
||||
// FindACMEProviderAccountName 根据主键查找名称
|
||||
func (this *ACMEProviderAccountDAO) FindACMEProviderAccountName(tx *dbs.Tx, id int64) (string, error) {
|
||||
return this.Query(tx).
|
||||
Pk(id).
|
||||
Result("name").
|
||||
FindStringCol("")
|
||||
}
|
||||
|
||||
// CreateAccount 创建账号
|
||||
func (this *ACMEProviderAccountDAO) CreateAccount(tx *dbs.Tx, userId int64, name string, providerCode string, eabKid string, eabKey string) (int64, error) {
|
||||
var op = NewACMEProviderAccountOperator()
|
||||
op.UserId = userId
|
||||
op.Name = name
|
||||
op.ProviderCode = providerCode
|
||||
op.EabKid = eabKid
|
||||
op.EabKey = eabKey
|
||||
|
||||
op.IsOn = true
|
||||
op.State = ACMEProviderAccountStateEnabled
|
||||
return this.SaveInt64(tx, op)
|
||||
}
|
||||
|
||||
// UpdateAccount 修改账号
|
||||
func (this *ACMEProviderAccountDAO) UpdateAccount(tx *dbs.Tx, accountId int64, name string, eabKid string, eabKey string) error {
|
||||
if accountId <= 0 {
|
||||
return errors.New("invalid accountId")
|
||||
}
|
||||
var op = NewACMEProviderAccountOperator()
|
||||
op.Id = accountId
|
||||
op.Name = name
|
||||
op.EabKid = eabKid
|
||||
op.EabKey = eabKey
|
||||
return this.Save(tx, op)
|
||||
}
|
||||
|
||||
// CountAllEnabledAccounts 计算账号数量
|
||||
func (this *ACMEProviderAccountDAO) CountAllEnabledAccounts(tx *dbs.Tx, userId int64) (int64, error) {
|
||||
return this.Query(tx).
|
||||
State(ACMEProviderAccountStateEnabled).
|
||||
Attr("userId", userId).
|
||||
Count()
|
||||
}
|
||||
|
||||
// ListEnabledAccounts 查找单页账号
|
||||
func (this *ACMEProviderAccountDAO) ListEnabledAccounts(tx *dbs.Tx, userId int64, offset int64, size int64) (result []*ACMEProviderAccount, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(ACMEProviderAccountStateEnabled).
|
||||
Attr("userId", userId).
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
DescPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllEnabledAccountsWithProviderCode 根据服务商代号查找账号
|
||||
func (this *ACMEProviderAccountDAO) FindAllEnabledAccountsWithProviderCode(tx *dbs.Tx, userId int64, providerCode string) (result []*ACMEProviderAccount, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(ACMEProviderAccountStateEnabled).
|
||||
Attr("providerCode", providerCode).
|
||||
Attr("userId", userId).
|
||||
DescPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// CheckUserAccount 检查是否为用户的服务商账号
|
||||
func (this *ACMEProviderAccountDAO) CheckUserAccount(tx *dbs.Tx, userId int64, accountId int64) error {
|
||||
if userId <= 0 || accountId <= 0 {
|
||||
return models.ErrNotFound
|
||||
}
|
||||
|
||||
b, err := this.Query(tx).
|
||||
Pk(accountId).
|
||||
State(ACMEProviderAccountStateEnabled).
|
||||
Attr("userId", userId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !b {
|
||||
return models.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package acme
|
||||
|
||||
// ACMEProviderAccount ACME提供商
|
||||
type ACMEProviderAccount struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
UserId uint64 `field:"userId"` // 用户ID
|
||||
IsOn bool `field:"isOn"` // 是否启用
|
||||
Name string `field:"name"` // 名称
|
||||
ProviderCode string `field:"providerCode"` // 代号
|
||||
EabKid string `field:"eabKid"` // KID
|
||||
EabKey string `field:"eabKey"` // Key
|
||||
Error string `field:"error"` // 最后一条错误信息
|
||||
State uint8 `field:"state"` // 状态
|
||||
}
|
||||
|
||||
type ACMEProviderAccountOperator struct {
|
||||
Id any // ID
|
||||
UserId any // 用户ID
|
||||
IsOn any // 是否启用
|
||||
Name any // 名称
|
||||
ProviderCode any // 代号
|
||||
EabKid any // KID
|
||||
EabKey any // Key
|
||||
Error any // 最后一条错误信息
|
||||
State any // 状态
|
||||
}
|
||||
|
||||
func NewACMEProviderAccountOperator() *ACMEProviderAccountOperator {
|
||||
return &ACMEProviderAccountOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package acme
|
||||
517
EdgeAPI/internal/db/models/acme/acme_task_dao.go
Normal file
517
EdgeAPI/internal/db/models/acme/acme_task_dao.go
Normal file
@@ -0,0 +1,517 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
acmeutils "github.com/TeaOSLab/EdgeAPI/internal/acme"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/dns"
|
||||
dbutils "github.com/TeaOSLab/EdgeAPI/internal/db/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ACMETaskStateEnabled = 1 // 已启用
|
||||
ACMETaskStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type ACMETaskDAO dbs.DAO
|
||||
|
||||
func NewACMETaskDAO() *ACMETaskDAO {
|
||||
return dbs.NewDAO(&ACMETaskDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeACMETasks",
|
||||
Model: new(ACMETask),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ACMETaskDAO)
|
||||
}
|
||||
|
||||
var SharedACMETaskDAO *ACMETaskDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedACMETaskDAO = NewACMETaskDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// EnableACMETask 启用条目
|
||||
func (this *ACMETaskDAO) EnableACMETask(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMETaskStateEnabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableACMETask 禁用条目
|
||||
func (this *ACMETaskDAO) DisableACMETask(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMETaskStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// FindEnabledACMETask 查找启用中的条目
|
||||
func (this *ACMETaskDAO) FindEnabledACMETask(tx *dbs.Tx, id int64) (*ACMETask, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Attr("state", ACMETaskStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*ACMETask), err
|
||||
}
|
||||
|
||||
// CountACMETasksWithACMEUserId 计算某个ACME用户相关的任务数量
|
||||
func (this *ACMETaskDAO) CountACMETasksWithACMEUserId(tx *dbs.Tx, acmeUserId int64) (int64, error) {
|
||||
return this.Query(tx).
|
||||
State(ACMETaskStateEnabled).
|
||||
Attr("acmeUserId", acmeUserId).
|
||||
Count()
|
||||
}
|
||||
|
||||
// CountACMETasksWithDNSProviderId 计算某个DNS服务商相关的任务数量
|
||||
func (this *ACMETaskDAO) CountACMETasksWithDNSProviderId(tx *dbs.Tx, dnsProviderId int64) (int64, error) {
|
||||
return this.Query(tx).
|
||||
State(ACMETaskStateEnabled).
|
||||
Attr("dnsProviderId", dnsProviderId).
|
||||
Count()
|
||||
}
|
||||
|
||||
// DisableAllTasksWithCertId 停止某个证书相关任务
|
||||
func (this *ACMETaskDAO) DisableAllTasksWithCertId(tx *dbs.Tx, certId int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Attr("certId", certId).
|
||||
Set("state", ACMETaskStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// CountAllEnabledACMETasks 计算所有任务数量
|
||||
func (this *ACMETaskDAO) CountAllEnabledACMETasks(tx *dbs.Tx, userId int64, isAvailable bool, isExpired bool, expiringDays int64, keyword string, userOnly bool) (int64, error) {
|
||||
var query = this.Query(tx)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
} else {
|
||||
if userOnly {
|
||||
query.Gt("userId", 0)
|
||||
} else {
|
||||
query.Attr("userId", 0)
|
||||
}
|
||||
}
|
||||
if isAvailable || isExpired || expiringDays > 0 {
|
||||
query.Gt("certId", 0)
|
||||
|
||||
if isAvailable {
|
||||
query.Where("certId IN (SELECT id FROM " + models.SharedSSLCertDAO.Table + " WHERE timeBeginAt<=UNIX_TIMESTAMP() AND timeEndAt>=UNIX_TIMESTAMP())")
|
||||
}
|
||||
if isExpired {
|
||||
query.Where("certId IN (SELECT id FROM " + models.SharedSSLCertDAO.Table + " WHERE timeEndAt<UNIX_TIMESTAMP())")
|
||||
}
|
||||
if expiringDays > 0 {
|
||||
query.Where("certId IN (SELECT id FROM "+models.SharedSSLCertDAO.Table+" WHERE timeEndAt>UNIX_TIMESTAMP() AND timeEndAt<:expiredAt)").
|
||||
Param("expiredAt", time.Now().Unix()+expiringDays*86400)
|
||||
}
|
||||
}
|
||||
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(domains LIKE :keyword)").
|
||||
Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("domains LIKE :keyword").
|
||||
Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
|
||||
return query.State(ACMETaskStateEnabled).
|
||||
Count()
|
||||
}
|
||||
|
||||
// ListEnabledACMETasks 列出单页任务
|
||||
func (this *ACMETaskDAO) ListEnabledACMETasks(tx *dbs.Tx, userId int64, isAvailable bool, isExpired bool, expiringDays int64, keyword string, userOnly bool, offset int64, size int64) (result []*ACMETask, err error) {
|
||||
var query = this.Query(tx)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
} else {
|
||||
if userOnly {
|
||||
query.Gt("userId", 0)
|
||||
} else {
|
||||
query.Attr("userId", 0)
|
||||
}
|
||||
}
|
||||
if isAvailable || isExpired || expiringDays > 0 {
|
||||
query.Gt("certId", 0)
|
||||
|
||||
if isAvailable {
|
||||
query.Where("certId IN (SELECT id FROM " + models.SharedSSLCertDAO.Table + " WHERE timeBeginAt<=UNIX_TIMESTAMP() AND timeEndAt>=UNIX_TIMESTAMP())")
|
||||
}
|
||||
if isExpired {
|
||||
query.Where("certId IN (SELECT id FROM " + models.SharedSSLCertDAO.Table + " WHERE timeEndAt<UNIX_TIMESTAMP())")
|
||||
}
|
||||
if expiringDays > 0 {
|
||||
query.Where("certId IN (SELECT id FROM "+models.SharedSSLCertDAO.Table+" WHERE timeEndAt>UNIX_TIMESTAMP() AND timeEndAt<:expiredAt)").
|
||||
Param("expiredAt", time.Now().Unix()+expiringDays*86400)
|
||||
}
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
query.Where("(domains LIKE :keyword)").
|
||||
Param("keyword", dbutils.QuoteLike(keyword))
|
||||
}
|
||||
_, err = query.
|
||||
State(ACMETaskStateEnabled).
|
||||
DescPk().
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// CreateACMETask 创建任务
|
||||
func (this *ACMETaskDAO) CreateACMETask(tx *dbs.Tx, adminId int64, userId int64, authType acmeutils.AuthType, acmeUserId int64, dnsProviderId int64, dnsDomain string, domains []string, autoRenew bool, authURL string) (int64, error) {
|
||||
var op = NewACMETaskOperator()
|
||||
op.AdminId = adminId
|
||||
op.UserId = userId
|
||||
op.AuthType = authType
|
||||
op.AcmeUserId = acmeUserId
|
||||
op.DnsProviderId = dnsProviderId
|
||||
op.DnsDomain = dnsDomain
|
||||
|
||||
if len(domains) > 0 {
|
||||
domainsJSON, err := json.Marshal(domains)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
op.Domains = domainsJSON
|
||||
} else {
|
||||
op.Domains = "[]"
|
||||
}
|
||||
|
||||
op.AutoRenew = autoRenew
|
||||
op.AuthURL = authURL
|
||||
op.IsOn = true
|
||||
op.State = ACMETaskStateEnabled
|
||||
err := this.Save(tx, op)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return types.Int64(op.Id), nil
|
||||
}
|
||||
|
||||
// UpdateACMETask 修改任务
|
||||
func (this *ACMETaskDAO) UpdateACMETask(tx *dbs.Tx, acmeTaskId int64, acmeUserId int64, dnsProviderId int64, dnsDomain string, domains []string, autoRenew bool, authURL string) error {
|
||||
if acmeTaskId <= 0 {
|
||||
return errors.New("invalid acmeTaskId")
|
||||
}
|
||||
|
||||
var op = NewACMETaskOperator()
|
||||
op.Id = acmeTaskId
|
||||
op.AcmeUserId = acmeUserId
|
||||
op.DnsProviderId = dnsProviderId
|
||||
op.DnsDomain = dnsDomain
|
||||
|
||||
if len(domains) > 0 {
|
||||
domainsJSON, err := json.Marshal(domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
op.Domains = domainsJSON
|
||||
} else {
|
||||
op.Domains = "[]"
|
||||
}
|
||||
|
||||
op.AutoRenew = autoRenew
|
||||
op.AuthURL = authURL
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckUserACMETask 检查用户权限
|
||||
func (this *ACMETaskDAO) CheckUserACMETask(tx *dbs.Tx, userId int64, acmeTaskId int64) (bool, error) {
|
||||
var query = this.Query(tx)
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
|
||||
return query.
|
||||
State(ACMETaskStateEnabled).
|
||||
Pk(acmeTaskId).
|
||||
Exist()
|
||||
}
|
||||
|
||||
// FindACMETaskUserId 查找任务所属用户ID
|
||||
func (this *ACMETaskDAO) FindACMETaskUserId(tx *dbs.Tx, taskId int64) (userId int64, err error) {
|
||||
return this.Query(tx).
|
||||
Pk(taskId).
|
||||
Result("userId").
|
||||
FindInt64Col(0)
|
||||
}
|
||||
|
||||
// UpdateACMETaskCert 设置任务关联的证书
|
||||
func (this *ACMETaskDAO) UpdateACMETaskCert(tx *dbs.Tx, taskId int64, certId int64) error {
|
||||
if taskId <= 0 {
|
||||
return errors.New("invalid taskId")
|
||||
}
|
||||
|
||||
var op = NewACMETaskOperator()
|
||||
op.Id = taskId
|
||||
op.CertId = certId
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunTask 执行任务并记录日志
|
||||
func (this *ACMETaskDAO) RunTask(tx *dbs.Tx, taskId int64) (isOk bool, errMsg string, resultCertId int64) {
|
||||
isOk, errMsg, resultCertId = this.runTaskWithoutLog(tx, taskId)
|
||||
|
||||
// 记录日志
|
||||
err := SharedACMETaskLogDAO.CreateACMETaskLog(tx, taskId, isOk, errMsg)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 执行任务但并不记录日志
|
||||
func (this *ACMETaskDAO) runTaskWithoutLog(tx *dbs.Tx, taskId int64) (isOk bool, errMsg string, resultCertId int64) {
|
||||
task, err := this.FindEnabledACMETask(tx, taskId)
|
||||
if err != nil {
|
||||
errMsg = "查询任务信息时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
if task == nil {
|
||||
errMsg = "找不到要执行的任务"
|
||||
return
|
||||
}
|
||||
if !task.IsOn {
|
||||
errMsg = "任务没有启用"
|
||||
return
|
||||
}
|
||||
|
||||
// ACME用户
|
||||
user, err := SharedACMEUserDAO.FindEnabledACMEUser(tx, int64(task.AcmeUserId))
|
||||
if err != nil {
|
||||
errMsg = "查询ACME用户时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
errMsg = "找不到ACME用户"
|
||||
return
|
||||
}
|
||||
|
||||
// 服务商
|
||||
if len(user.ProviderCode) == 0 {
|
||||
user.ProviderCode = acmeutils.DefaultProviderCode
|
||||
}
|
||||
var acmeProvider = acmeutils.FindProviderWithCode(user.ProviderCode)
|
||||
if acmeProvider == nil {
|
||||
errMsg = "服务商已不可用"
|
||||
return
|
||||
}
|
||||
|
||||
// 账号
|
||||
var acmeAccount *acmeutils.Account
|
||||
if user.AccountId > 0 {
|
||||
account, err := SharedACMEProviderAccountDAO.FindEnabledACMEProviderAccount(tx, int64(user.AccountId))
|
||||
if err != nil {
|
||||
errMsg = "查询ACME账号时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
if account != nil {
|
||||
acmeAccount = &acmeutils.Account{
|
||||
EABKid: account.EabKid,
|
||||
EABKey: account.EabKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
privateKey, err := acmeutils.ParsePrivateKeyFromBase64(user.PrivateKey)
|
||||
if err != nil {
|
||||
errMsg = "解析私钥时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
var remoteUser = acmeutils.NewUser(user.Email, privateKey, func(resource *registration.Resource) error {
|
||||
resourceJSON, err := json.Marshal(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = SharedACMEUserDAO.UpdateACMEUserRegistration(tx, int64(user.Id), resourceJSON)
|
||||
return err
|
||||
})
|
||||
|
||||
if len(user.Registration) > 0 {
|
||||
err = remoteUser.SetRegistration(user.Registration)
|
||||
if err != nil {
|
||||
errMsg = "设置注册信息时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var acmeTask *acmeutils.Task = nil
|
||||
if task.AuthType == acmeutils.AuthTypeDNS {
|
||||
// DNS服务商
|
||||
dnsProvider, err := dns.SharedDNSProviderDAO.FindEnabledDNSProvider(tx, int64(task.DnsProviderId))
|
||||
if err != nil {
|
||||
errMsg = "查找DNS服务商账号信息时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
if dnsProvider == nil {
|
||||
errMsg = "找不到DNS服务商账号"
|
||||
return
|
||||
}
|
||||
providerInterface := dnsclients.FindProvider(dnsProvider.Type, int64(dnsProvider.Id))
|
||||
if providerInterface == nil {
|
||||
errMsg = "暂不支持此类型的DNS服务商 '" + dnsProvider.Type + "'"
|
||||
return
|
||||
}
|
||||
providerInterface.SetMinTTL(int32(dnsProvider.MinTTL))
|
||||
apiParams, err := dnsProvider.DecodeAPIParams()
|
||||
if err != nil {
|
||||
errMsg = "解析DNS服务商API参数时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
err = providerInterface.Auth(apiParams)
|
||||
if err != nil {
|
||||
errMsg = "校验DNS服务商API参数时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
acmeTask = &acmeutils.Task{
|
||||
User: remoteUser,
|
||||
AuthType: acmeutils.AuthTypeDNS,
|
||||
DNSProvider: providerInterface,
|
||||
DNSDomain: task.DnsDomain,
|
||||
Domains: task.DecodeDomains(),
|
||||
}
|
||||
} else if task.AuthType == acmeutils.AuthTypeHTTP {
|
||||
acmeTask = &acmeutils.Task{
|
||||
User: remoteUser,
|
||||
AuthType: acmeutils.AuthTypeHTTP,
|
||||
Domains: task.DecodeDomains(),
|
||||
}
|
||||
}
|
||||
acmeTask.Provider = acmeProvider
|
||||
acmeTask.Account = acmeAccount
|
||||
|
||||
var acmeRequest = acmeutils.NewRequest(acmeTask)
|
||||
acmeRequest.OnAuth(func(domain, token, keyAuth string) {
|
||||
err := SharedACMEAuthenticationDAO.CreateAuth(tx, taskId, domain, token, keyAuth)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACME", "write authentication to database error: "+err.Error())
|
||||
} else {
|
||||
// 调用校验URL
|
||||
if len(task.AuthURL) > 0 {
|
||||
authJSON, err := json.Marshal(maps.Map{
|
||||
"domain": domain,
|
||||
"token": token,
|
||||
"key": keyAuth,
|
||||
})
|
||||
if err != nil {
|
||||
remotelogs.Error("ACME", "encode auth data failed: '"+task.AuthURL+"'")
|
||||
} else {
|
||||
var client = utils.SharedHttpClient(10 * time.Second)
|
||||
req, err := http.NewRequest(http.MethodPost, task.AuthURL, bytes.NewReader(authJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACME", "parse auth url failed '"+task.AuthURL+"': "+err.Error())
|
||||
} else {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
remotelogs.Error("ACME", "call auth url failed '"+task.AuthURL+"': "+err.Error())
|
||||
} else {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
certData, keyData, err := acmeRequest.Run()
|
||||
if err != nil {
|
||||
errMsg = "证书生成失败:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
// 分析证书
|
||||
var sslConfig = &sslconfigs.SSLCertConfig{
|
||||
CertData: certData,
|
||||
KeyData: keyData,
|
||||
}
|
||||
err = sslConfig.Init(context.Background())
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,但是分析证书信息时发生错误:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
// 保存证书
|
||||
resultCertId = int64(task.CertId)
|
||||
if resultCertId > 0 {
|
||||
cert, err := models.SharedSSLCertDAO.FindEnabledSSLCert(tx, resultCertId)
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,但查询已绑定的证书时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
if cert == nil {
|
||||
errMsg = "证书已被管理员或用户删除"
|
||||
|
||||
// 禁用
|
||||
err = SharedACMETaskDAO.DisableACMETask(tx, taskId)
|
||||
if err != nil {
|
||||
errMsg = "禁用失效的ACME任务出错:" + err.Error()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = models.SharedSSLCertDAO.UpdateCert(tx, resultCertId, cert.IsOn, cert.Name, cert.Description, cert.ServerName, cert.IsCA, certData, keyData, sslConfig.TimeBeginAt, sslConfig.TimeEndAt, sslConfig.DNSNames, sslConfig.CommonNames)
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,但是修改数据库中的证书信息时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
resultCertId, err = models.SharedSSLCertDAO.CreateCert(tx, int64(task.AdminId), int64(task.UserId), true, task.DnsDomain+"免费证书", "免费申请的证书", "", false, certData, keyData, sslConfig.TimeBeginAt, sslConfig.TimeEndAt, sslConfig.DNSNames, sslConfig.CommonNames)
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,但是保存到数据库失败:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
err = models.SharedSSLCertDAO.UpdateCertACME(tx, resultCertId, int64(task.Id))
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,修改证书ACME信息时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置成功
|
||||
err = SharedACMETaskDAO.UpdateACMETaskCert(tx, taskId, resultCertId)
|
||||
if err != nil {
|
||||
errMsg = "证书生成成功,设置任务关联的证书时出错:" + err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isOk = true
|
||||
return
|
||||
}
|
||||
5
EdgeAPI/internal/db/models/acme/acme_task_dao_test.go
Normal file
5
EdgeAPI/internal/db/models/acme/acme_task_dao_test.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
51
EdgeAPI/internal/db/models/acme/acme_task_log_dao.go
Normal file
51
EdgeAPI/internal/db/models/acme/acme_task_log_dao.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
type ACMETaskLogDAO dbs.DAO
|
||||
|
||||
func NewACMETaskLogDAO() *ACMETaskLogDAO {
|
||||
return dbs.NewDAO(&ACMETaskLogDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeACMETaskLogs",
|
||||
Model: new(ACMETaskLog),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ACMETaskLogDAO)
|
||||
}
|
||||
|
||||
var SharedACMETaskLogDAO *ACMETaskLogDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedACMETaskLogDAO = NewACMETaskLogDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// CreateACMETaskLog 生成日志
|
||||
func (this *ACMETaskLogDAO) CreateACMETaskLog(tx *dbs.Tx, taskId int64, isOk bool, errMsg string) error {
|
||||
var op = NewACMETaskLogOperator()
|
||||
op.TaskId = taskId
|
||||
op.Error = utils.LimitString(errMsg, 1024)
|
||||
op.IsOk = isOk
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// FindLatestACMETasKLog 取得任务的最后一条执行日志
|
||||
func (this *ACMETaskLogDAO) FindLatestACMETasKLog(tx *dbs.Tx, taskId int64) (*ACMETaskLog, error) {
|
||||
one, err := this.Query(tx).
|
||||
Attr("taskId", taskId).
|
||||
DescPk().
|
||||
Find()
|
||||
if err != nil || one == nil {
|
||||
return nil, err
|
||||
}
|
||||
return one.(*ACMETaskLog), nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
22
EdgeAPI/internal/db/models/acme/acme_task_log_model.go
Normal file
22
EdgeAPI/internal/db/models/acme/acme_task_log_model.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package acme
|
||||
|
||||
// ACMETaskLog ACME任务运行日志
|
||||
type ACMETaskLog struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
TaskId uint64 `field:"taskId"` // 任务ID
|
||||
IsOk bool `field:"isOk"` // 是否成功
|
||||
Error string `field:"error"` // 错误信息
|
||||
CreatedAt uint64 `field:"createdAt"` // 运行时间
|
||||
}
|
||||
|
||||
type ACMETaskLogOperator struct {
|
||||
Id interface{} // ID
|
||||
TaskId interface{} // 任务ID
|
||||
IsOk interface{} // 是否成功
|
||||
Error interface{} // 错误信息
|
||||
CreatedAt interface{} // 运行时间
|
||||
}
|
||||
|
||||
func NewACMETaskLogOperator() *ACMETaskLogOperator {
|
||||
return &ACMETaskLogOperator{}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package acme
|
||||
42
EdgeAPI/internal/db/models/acme/acme_task_model.go
Normal file
42
EdgeAPI/internal/db/models/acme/acme_task_model.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package acme
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// ACMETask ACME任务
|
||||
type ACMETask struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
AdminId uint32 `field:"adminId"` // 管理员ID
|
||||
UserId uint32 `field:"userId"` // 用户ID
|
||||
IsOn bool `field:"isOn"` // 是否启用
|
||||
AcmeUserId uint32 `field:"acmeUserId"` // ACME用户ID
|
||||
DnsDomain string `field:"dnsDomain"` // DNS主域名
|
||||
DnsProviderId uint64 `field:"dnsProviderId"` // DNS服务商
|
||||
Domains dbs.JSON `field:"domains"` // 证书域名
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
State uint8 `field:"state"` // 状态
|
||||
CertId uint64 `field:"certId"` // 生成的证书ID
|
||||
AutoRenew uint8 `field:"autoRenew"` // 是否自动更新
|
||||
AuthType string `field:"authType"` // 认证类型
|
||||
AuthURL string `field:"authURL"` // 认证URL
|
||||
}
|
||||
|
||||
type ACMETaskOperator struct {
|
||||
Id interface{} // ID
|
||||
AdminId interface{} // 管理员ID
|
||||
UserId interface{} // 用户ID
|
||||
IsOn interface{} // 是否启用
|
||||
AcmeUserId interface{} // ACME用户ID
|
||||
DnsDomain interface{} // DNS主域名
|
||||
DnsProviderId interface{} // DNS服务商
|
||||
Domains interface{} // 证书域名
|
||||
CreatedAt interface{} // 创建时间
|
||||
State interface{} // 状态
|
||||
CertId interface{} // 生成的证书ID
|
||||
AutoRenew interface{} // 是否自动更新
|
||||
AuthType interface{} // 认证类型
|
||||
AuthURL interface{} // 认证URL
|
||||
}
|
||||
|
||||
func NewACMETaskOperator() *ACMETaskOperator {
|
||||
return &ACMETaskOperator{}
|
||||
}
|
||||
20
EdgeAPI/internal/db/models/acme/acme_task_model_ext.go
Normal file
20
EdgeAPI/internal/db/models/acme/acme_task_model_ext.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
)
|
||||
|
||||
// DecodeDomains 将域名解析成字符串数组
|
||||
func (this *ACMETask) DecodeDomains() []string {
|
||||
if len(this.Domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := []string{}
|
||||
err := json.Unmarshal(this.Domains, &result)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
211
EdgeAPI/internal/db/models/acme/acme_user_dao.go
Normal file
211
EdgeAPI/internal/db/models/acme/acme_user_dao.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
)
|
||||
|
||||
const (
|
||||
ACMEUserStateEnabled = 1 // 已启用
|
||||
ACMEUserStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type ACMEUserDAO dbs.DAO
|
||||
|
||||
func NewACMEUserDAO() *ACMEUserDAO {
|
||||
return dbs.NewDAO(&ACMEUserDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeACMEUsers",
|
||||
Model: new(ACMEUser),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ACMEUserDAO)
|
||||
}
|
||||
|
||||
var SharedACMEUserDAO *ACMEUserDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedACMEUserDAO = NewACMEUserDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// EnableACMEUser 启用条目
|
||||
func (this *ACMEUserDAO) EnableACMEUser(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMEUserStateEnabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableACMEUser 禁用条目
|
||||
func (this *ACMEUserDAO) DisableACMEUser(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ACMEUserStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// 查找启用中的条目
|
||||
func (this *ACMEUserDAO) FindEnabledACMEUser(tx *dbs.Tx, id int64) (*ACMEUser, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Attr("state", ACMEUserStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*ACMEUser), err
|
||||
}
|
||||
|
||||
// CreateACMEUser 创建用户
|
||||
func (this *ACMEUserDAO) CreateACMEUser(tx *dbs.Tx, adminId int64, userId int64, providerCode string, accountId int64, email string, description string) (int64, error) {
|
||||
// 生成私钥
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
privateKeyData, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
privateKeyText := base64.StdEncoding.EncodeToString(privateKeyData)
|
||||
|
||||
var op = NewACMEUserOperator()
|
||||
op.AdminId = adminId
|
||||
op.UserId = userId
|
||||
op.ProviderCode = providerCode
|
||||
op.AccountId = accountId
|
||||
op.Email = email
|
||||
op.Description = description
|
||||
op.PrivateKey = privateKeyText
|
||||
op.State = ACMEUserStateEnabled
|
||||
err = this.Save(tx, op)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return types.Int64(op.Id), nil
|
||||
}
|
||||
|
||||
// UpdateACMEUser 修改用户信息
|
||||
func (this *ACMEUserDAO) UpdateACMEUser(tx *dbs.Tx, acmeUserId int64, description string) error {
|
||||
if acmeUserId <= 0 {
|
||||
return errors.New("invalid acmeUserId")
|
||||
}
|
||||
var op = NewACMEUserOperator()
|
||||
op.Id = acmeUserId
|
||||
op.Description = description
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateACMEUserRegistration 修改用户ACME注册信息
|
||||
func (this *ACMEUserDAO) UpdateACMEUserRegistration(tx *dbs.Tx, acmeUserId int64, registrationJSON []byte) error {
|
||||
if acmeUserId <= 0 {
|
||||
return errors.New("invalid acmeUserId")
|
||||
}
|
||||
var op = NewACMEUserOperator()
|
||||
op.Id = acmeUserId
|
||||
op.Registration = registrationJSON
|
||||
err := this.Save(tx, op)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountACMEUsersWithAdminId 计算用户数量
|
||||
func (this *ACMEUserDAO) CountACMEUsersWithAdminId(tx *dbs.Tx, adminId int64, userId int64, accountId int64) (int64, error) {
|
||||
query := this.Query(tx)
|
||||
if adminId > 0 {
|
||||
query.Attr("adminId", adminId)
|
||||
}
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
} else {
|
||||
query.Attr("userId", 0)
|
||||
}
|
||||
if accountId > 0 {
|
||||
query.Attr("accountId", accountId)
|
||||
}
|
||||
|
||||
return query.
|
||||
State(ACMEUserStateEnabled).
|
||||
Count()
|
||||
}
|
||||
|
||||
// ListACMEUsers 列出当前管理员的用户
|
||||
func (this *ACMEUserDAO) ListACMEUsers(tx *dbs.Tx, adminId int64, userId int64, offset int64, size int64) (result []*ACMEUser, err error) {
|
||||
query := this.Query(tx)
|
||||
if adminId > 0 {
|
||||
query.Attr("adminId", adminId)
|
||||
}
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
} else {
|
||||
query.Attr("userId", 0)
|
||||
}
|
||||
|
||||
_, err = query.
|
||||
State(ACMEUserStateEnabled).
|
||||
Offset(offset).
|
||||
Limit(size).
|
||||
Slice(&result).
|
||||
DescPk().
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllACMEUsers 查找所有用户
|
||||
func (this *ACMEUserDAO) FindAllACMEUsers(tx *dbs.Tx, adminId int64, userId int64, providerCode string) (result []*ACMEUser, err error) {
|
||||
// 防止没有传入条件导致返回的数据过多
|
||||
if adminId <= 0 && userId <= 0 {
|
||||
return nil, errors.New("'adminId' or 'userId' should not be empty")
|
||||
}
|
||||
|
||||
query := this.Query(tx)
|
||||
if adminId > 0 {
|
||||
query.Attr("adminId", adminId)
|
||||
}
|
||||
if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
}
|
||||
if len(providerCode) > 0 {
|
||||
query.Attr("providerCode", providerCode)
|
||||
}
|
||||
_, err = query.
|
||||
State(ACMEUserStateEnabled).
|
||||
Slice(&result).
|
||||
DescPk().
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// CheckACMEUser 检查用户权限
|
||||
func (this *ACMEUserDAO) CheckACMEUser(tx *dbs.Tx, acmeUserId int64, adminId int64, userId int64) (bool, error) {
|
||||
if acmeUserId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
query := this.Query(tx)
|
||||
if adminId > 0 {
|
||||
query.Attr("adminId", adminId)
|
||||
} else if userId > 0 {
|
||||
query.Attr("userId", userId)
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return query.
|
||||
State(ACMEUserStateEnabled).
|
||||
Exist()
|
||||
}
|
||||
5
EdgeAPI/internal/db/models/acme/acme_user_dao_test.go
Normal file
5
EdgeAPI/internal/db/models/acme/acme_user_dao_test.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
36
EdgeAPI/internal/db/models/acme/acme_user_model.go
Normal file
36
EdgeAPI/internal/db/models/acme/acme_user_model.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package acme
|
||||
|
||||
import "github.com/iwind/TeaGo/dbs"
|
||||
|
||||
// ACMEUser ACME用户
|
||||
type ACMEUser struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
AdminId uint32 `field:"adminId"` // 管理员ID
|
||||
UserId uint32 `field:"userId"` // 用户ID
|
||||
PrivateKey string `field:"privateKey"` // 私钥
|
||||
Email string `field:"email"` // E-mail
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
State uint8 `field:"state"` // 状态
|
||||
Description string `field:"description"` // 备注介绍
|
||||
Registration dbs.JSON `field:"registration"` // 注册信息
|
||||
ProviderCode string `field:"providerCode"` // 服务商代号
|
||||
AccountId uint64 `field:"accountId"` // 提供商ID
|
||||
}
|
||||
|
||||
type ACMEUserOperator struct {
|
||||
Id interface{} // ID
|
||||
AdminId interface{} // 管理员ID
|
||||
UserId interface{} // 用户ID
|
||||
PrivateKey interface{} // 私钥
|
||||
Email interface{} // E-mail
|
||||
CreatedAt interface{} // 创建时间
|
||||
State interface{} // 状态
|
||||
Description interface{} // 备注介绍
|
||||
Registration interface{} // 注册信息
|
||||
ProviderCode interface{} // 服务商代号
|
||||
AccountId interface{} // 提供商ID
|
||||
}
|
||||
|
||||
func NewACMEUserOperator() *ACMEUserOperator {
|
||||
return &ACMEUserOperator{}
|
||||
}
|
||||
1
EdgeAPI/internal/db/models/acme/acme_user_model_ext.go
Normal file
1
EdgeAPI/internal/db/models/acme/acme_user_model_ext.go
Normal file
@@ -0,0 +1 @@
|
||||
package acme
|
||||
71
EdgeAPI/internal/db/models/ad_network_dao.go
Normal file
71
EdgeAPI/internal/db/models/ad_network_dao.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
const (
|
||||
ADNetworkStateEnabled = 1 // 已启用
|
||||
ADNetworkStateDisabled = 0 // 已禁用
|
||||
)
|
||||
|
||||
type ADNetworkDAO dbs.DAO
|
||||
|
||||
func NewADNetworkDAO() *ADNetworkDAO {
|
||||
return dbs.NewDAO(&ADNetworkDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeADNetworks",
|
||||
Model: new(ADNetwork),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ADNetworkDAO)
|
||||
}
|
||||
|
||||
var SharedADNetworkDAO *ADNetworkDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedADNetworkDAO = NewADNetworkDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// EnableADNetwork 启用条目
|
||||
func (this *ADNetworkDAO) EnableADNetwork(tx *dbs.Tx, id uint32) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ADNetworkStateEnabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableADNetwork 禁用条目
|
||||
func (this *ADNetworkDAO) DisableADNetwork(tx *dbs.Tx, id int64) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(id).
|
||||
Set("state", ADNetworkStateDisabled).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// FindEnabledADNetwork 查找启用中的条目
|
||||
func (this *ADNetworkDAO) FindEnabledADNetwork(tx *dbs.Tx, id int64) (*ADNetwork, error) {
|
||||
result, err := this.Query(tx).
|
||||
Pk(id).
|
||||
State(ADNetworkStateEnabled).
|
||||
Find()
|
||||
if result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*ADNetwork), err
|
||||
}
|
||||
|
||||
// FindADNetworkName 根据主键查找名称
|
||||
func (this *ADNetworkDAO) FindADNetworkName(tx *dbs.Tx, id uint32) (string, error) {
|
||||
return this.Query(tx).
|
||||
Pk(id).
|
||||
Result("name").
|
||||
FindStringCol("")
|
||||
}
|
||||
56
EdgeAPI/internal/db/models/ad_network_dao_plus.go
Normal file
56
EdgeAPI/internal/db/models/ad_network_dao_plus.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
// CreateNetwork 创建线路
|
||||
func (this *ADNetworkDAO) CreateNetwork(tx *dbs.Tx, name string, description string) (int64, error) {
|
||||
var op = NewADNetworkOperator()
|
||||
op.Name = name
|
||||
op.Description = description
|
||||
op.IsOn = true
|
||||
op.State = ADNetworkStateEnabled
|
||||
return this.SaveInt64(tx, op)
|
||||
}
|
||||
|
||||
// UpdateNetwork 修改线路
|
||||
func (this *ADNetworkDAO) UpdateNetwork(tx *dbs.Tx, networkId int64, isOn bool, name string, description string) error {
|
||||
if networkId <= 0 {
|
||||
return errors.New("invalid networkId")
|
||||
}
|
||||
|
||||
var op = NewADNetworkOperator()
|
||||
op.Id = networkId
|
||||
op.Name = name
|
||||
op.Description = description
|
||||
op.IsOn = isOn
|
||||
return this.Save(tx, op)
|
||||
}
|
||||
|
||||
// FindAllNetworks 列出所有线路
|
||||
func (this *ADNetworkDAO) FindAllNetworks(tx *dbs.Tx) (result []*ADNetwork, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(ADNetworkStateEnabled).
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllAvailableNetworks 列出所有可用的线路
|
||||
func (this *ADNetworkDAO) FindAllAvailableNetworks(tx *dbs.Tx) (result []*ADNetwork, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(ADNetworkStateEnabled).
|
||||
Attr("isOn", true).
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
22
EdgeAPI/internal/db/models/ad_network_dao_plus_test.go
Normal file
22
EdgeAPI/internal/db/models/ad_network_dao_plus_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build plus
|
||||
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestADNetworkDAO_FindAllNetworks(t *testing.T) {
|
||||
var dao = models.NewADNetworkDAO()
|
||||
var tx *dbs.Tx
|
||||
networks, err := dao.FindAllNetworks(tx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logs.PrintAsJSON(networks, t)
|
||||
}
|
||||
6
EdgeAPI/internal/db/models/ad_network_dao_test.go
Normal file
6
EdgeAPI/internal/db/models/ad_network_dao_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user