Initial commit

This commit is contained in:
Yuzuki616
2024-09-12 06:04:32 +09:00
commit 3f58fa7f0d
31 changed files with 2814 additions and 0 deletions

40
acme/acme.go Normal file
View File

@@ -0,0 +1,40 @@
package acme
import (
"Ratte/conf"
"fmt"
"path"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
)
type Acme struct {
client *lego.Client
c *conf.ACME
}
func NewAcme(c *conf.ACME) (*Acme, error) {
user, err := NewLegoUser(
path.Join(c.Storage, "user", fmt.Sprintf("user-%s.json", c.Email)),
c.Email)
if err != nil {
return nil, fmt.Errorf("create user error: %s", err)
}
lc := lego.NewConfig(user)
//c.CADirURL = "http://192.168.99.100:4000/directory"
lc.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(lc)
if err != nil {
return nil, err
}
l := Acme{
client: client,
c: c,
}
err = l.SetProvider()
if err != nil {
return nil, fmt.Errorf("set provider error: %s", err)
}
return &l, nil
}

119
acme/cert.go Normal file
View File

@@ -0,0 +1,119 @@
package acme
import (
"Ratte/common/file"
"fmt"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/providers/dns"
"os"
"path"
"time"
)
func checkPath(p string) error {
if !file.IsExist(path.Dir(p)) {
err := os.MkdirAll(path.Dir(p), 0755)
if err != nil {
return fmt.Errorf("create dir error: %s", err)
}
}
return nil
}
func (l *Acme) SetProvider() error {
switch l.c.Provider {
case "http":
err := l.client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80"))
if err != nil {
return err
}
case "dns":
for k, v := range l.c.DNSEnv {
os.Setenv(k, v)
}
p, err := dns.NewDNSChallengeProviderByName(l.c.Provider)
if err != nil {
return fmt.Errorf("create dns challenge provider error: %s", err)
}
err = l.client.Challenge.SetDNS01Provider(p)
if err != nil {
return fmt.Errorf("set dns provider error: %s", err)
}
default:
return fmt.Errorf("unsupported provider %s", l.c.Provider)
}
return nil
}
func (l *Acme) CreateCert(certPath, keyPath, domain string) (err error) {
if certPath == "" || keyPath == "" {
return fmt.Errorf("cert file path or key file path not exist")
}
if file.IsExist(certPath) && file.IsExist(keyPath) {
return l.RenewCert(certPath, keyPath, domain)
}
request := certificate.ObtainRequest{
Domains: []string{domain},
Bundle: true,
}
certificates, err := l.client.Certificate.Obtain(request)
if err != nil {
return fmt.Errorf("obtain certificate error: %s", err)
}
err = l.writeCert(certPath, keyPath, certificates)
return nil
}
func (l *Acme) RenewCert(certPath, keyPath, domain string) error {
file, err := os.ReadFile(certPath)
if err != nil {
return fmt.Errorf("read cert file error: %s", err)
}
if e, err := l.CheckCert(file); !e {
return nil
} else if err != nil {
return fmt.Errorf("check cert error: %s", err)
}
res, err := l.client.Certificate.Renew(certificate.Resource{
Domain: domain,
Certificate: file,
}, true, false, "")
if err != nil {
return err
}
err = l.writeCert(certPath, keyPath, res)
return nil
}
func (l *Acme) CheckCert(file []byte) (bool, error) {
cert, err := certcrypto.ParsePEMCertificate(file)
if err != nil {
return false, err
}
notAfter := int(time.Until(cert.NotAfter).Hours() / 24.0)
if notAfter > 30 {
return false, nil
}
return true, nil
}
func (l *Acme) writeCert(cert, key string, certificates *certificate.Resource) error {
err := checkPath(cert)
if err != nil {
return fmt.Errorf("check path error: %s", err)
}
err = os.WriteFile(cert, certificates.Certificate, 0644)
if err != nil {
return err
}
err = checkPath(key)
if err != nil {
return fmt.Errorf("check path error: %s", err)
}
err = os.WriteFile(key, certificates.PrivateKey, 0644)
if err != nil {
return err
}
return nil
}

129
acme/user.go Normal file
View File

@@ -0,0 +1,129 @@
package acme
import (
"Ratte/common/file"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/goccy/go-json"
"os"
)
type User struct {
Email string `json:"Email"`
Registration *registration.Resource `json:"Registration"`
key crypto.PrivateKey
KeyEncoded string `json:"Key"`
}
func (u *User) GetEmail() string {
return u.Email
}
func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
func NewLegoUser(path string, email string) (*User, error) {
var user User
if file.IsExist(path) {
err := user.Load(path)
if err != nil {
return nil, err
}
if user.Email != email {
user.Registration = nil
user.Email = email
err := registerUser(&user, path)
if err != nil {
return nil, err
}
}
} else {
user.Email = email
err := registerUser(&user, path)
if err != nil {
return nil, err
}
}
return &user, nil
}
func registerUser(user *User, path string) error {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("generate key error: %s", err)
}
user.key = privateKey
c := lego.NewConfig(user)
client, err := lego.NewClient(c)
if err != nil {
return fmt.Errorf("create lego client error: %s", err)
}
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return err
}
user.Registration = reg
err = user.Save(path)
if err != nil {
return fmt.Errorf("save user error: %s", err)
}
return nil
}
func EncodePrivate(privKey *ecdsa.PrivateKey) (string, error) {
encoded, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
return "", err
}
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: encoded})
return string(pemEncoded), nil
}
func (u *User) Save(path string) error {
err := checkPath(path)
if err != nil {
return fmt.Errorf("check path error: %s", err)
}
u.KeyEncoded, _ = EncodePrivate(u.key.(*ecdsa.PrivateKey))
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
err = json.NewEncoder(f).Encode(u)
if err != nil {
return fmt.Errorf("marshal json error: %s", err)
}
u.KeyEncoded = ""
return nil
}
func (u *User) DecodePrivate(pemEncodedPriv string) (*ecdsa.PrivateKey, error) {
blockPriv, _ := pem.Decode([]byte(pemEncodedPriv))
x509EncodedPriv := blockPriv.Bytes
privateKey, err := x509.ParseECPrivateKey(x509EncodedPriv)
return privateKey, err
}
func (u *User) Load(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open file error: %s", err)
}
err = json.NewDecoder(f).Decode(u)
if err != nil {
return fmt.Errorf("unmarshal json error: %s", err)
}
u.key, err = u.DecodePrivate(u.KeyEncoded)
if err != nil {
return fmt.Errorf("decode private key error: %s", err)
}
return nil
}

32
cmd/ratte/ratte.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
pre "github.com/x-cray/logrus-prefixed-formatter"
)
var version string
var buildDate string
var command = &cobra.Command{
Use: "Ratte",
}
func Execute() {
err := command.Execute()
if err != nil {
log.WithField("err", err).Error("Execute command failed")
}
}
func main() {
log.SetFormatter(&pre.TextFormatter{
TimestampFormat: "01-02 15:04:05",
FullTimestamp: true,
})
log.Info("Ratte")
log.Info("Version: ", version)
log.Info("Build date: ", buildDate)
Execute()
}

150
cmd/ratte/run.go Normal file
View File

@@ -0,0 +1,150 @@
package main
import (
"Ratte/acme"
"Ratte/conf"
"Ratte/handler"
"Ratte/trigger"
"github.com/Yuzuki616/Ratte-Interface/core"
"github.com/Yuzuki616/Ratte-Interface/panel"
"github.com/Yuzuki616/Ratte-Interface/plugin"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
)
var runCommand = cobra.Command{
Use: "server",
Short: "Run Ratte",
Run: runHandle,
Args: cobra.NoArgs,
}
var config string
func init() {
runCommand.PersistentFlags().
StringVarP(&config, "config", "c",
"./config.json5", "config file path")
command.AddCommand(&runCommand)
}
var (
cores map[string]*plugin.Client[core.Core]
panels map[string]*plugin.Client[panel.Panel]
acmes map[string]*acme.Acme
handlers []*handler.Handler
triggers []*trigger.Trigger
)
func runHandle(_ *cobra.Command, _ []string) {
c := conf.New(config)
log.WithField("path", config).Info("Load config...")
err := c.Load(nil)
if err != nil {
log.WithError(err).Fatal("Load config failed")
}
log.WithField("path", config).Info("Loaded.")
log.Info("Init core plugin...")
// new cores
cores = make(map[string]*plugin.Client[core.Core], len(c.Core))
for _, co := range c.Core {
c, err := plugin.NewClient[core.Core](&plugin.Config{
Type: plugin.CoreType,
Cmd: exec.Command(co.Path),
})
if err != nil {
log.WithError(err).WithField("core", co.Name).Fatal("New core failed")
}
err = c.Caller().Start(co.DataPath, co.Config)
if err != nil {
log.WithError(err).WithField("core", co.Name).Fatal("Start core failed")
}
cores[co.Name] = c
}
log.Info("Done.")
log.Info("Init panel plugin...")
// new panels
panels = make(map[string]*plugin.Client[panel.Panel], len(c.Panel))
for _, p := range c.Panel {
pn, err := plugin.NewClient[panel.Panel](&plugin.Config{
Type: plugin.PanelType,
Cmd: exec.Command(p.Path),
})
if err != nil {
log.WithError(err).WithField("panel", p.Name).Fatal("New panel failed")
}
panels[p.Name] = pn
}
log.Info("Done.")
log.Info("Init acme...")
// new acme
acmes = make(map[string]*acme.Acme)
for _, a := range c.Acme {
ac, err := acme.NewAcme(&a)
if err != nil {
log.WithError(err).Fatal("New acme failed")
}
acmes[a.Name] = ac
}
log.Info("Done.")
log.Info("Starting...")
// new node
triggers = make([]*trigger.Trigger, 0, len(c.Node))
handlers = make([]*handler.Handler, len(c.Node))
for _, nd := range c.Node {
var co core.Core
var pl panel.Panel
var ac *acme.Acme
if c, ok := cores[nd.Options.Core]; ok {
co = c.Caller()
} else {
log.WithField("core", nd.Options.Core).Fatal("Couldn't find core")
}
if p, ok := panels[nd.Options.Panel]; ok {
pl = p.Caller()
} else {
log.WithField("panel", nd.Options.Panel).Fatal("Couldn't find panel")
}
if a, ok := acmes[nd.Options.Acme]; ok {
ac = a
} else {
log.WithField("acme", nd.Options.Acme).Fatal("Couldn't find acme")
}
h := handler.New(co, pl, nd.Name, ac, log.WithFields(
map[string]interface{}{
"node": nd.Name,
"service": "handler",
},
), &nd.Options)
handlers = append(handlers, h)
tr, err := trigger.NewTrigger(log.WithFields(
map[string]interface{}{
"node": nd.Name,
"service": "trigger",
},
), &nd.Trigger, h, pl, &nd.Remote)
if err != nil {
log.WithError(err).Fatal("New trigger failed")
}
triggers = append(triggers, tr)
err = tr.Start()
if err != nil {
log.WithError(err).Fatal("Start trigger failed")
}
}
log.Info("Started.")
runtime.GC()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM)
<-sig
}

8
common/file/file.go Normal file
View File

@@ -0,0 +1,8 @@
package file
import "os"
func IsExist(path string) bool {
_, err := os.Stat(path)
return err == nil || !os.IsNotExist(err)
}

112
common/json/trim.go Normal file
View File

@@ -0,0 +1,112 @@
package json
import (
"bytes"
"io"
)
type TrimNodeReader struct {
r io.Reader
br *bytes.Reader
}
func isNL(c byte) bool {
return c == '\n' || c == '\r'
}
func isWS(c byte) bool {
return c == ' ' || c == '\t' || isNL(c)
}
func consumeComment(s []byte, i int) int {
if i < len(s) && s[i] == '/' {
s[i-1] = ' '
for ; i < len(s) && !isNL(s[i]); i += 1 {
s[i] = ' '
}
}
if i < len(s) && s[i] == '*' {
s[i-1] = ' '
s[i] = ' '
for ; i < len(s); i += 1 {
if s[i] != '*' {
s[i] = ' '
} else {
s[i] = ' '
i++
if i < len(s) {
if s[i] == '/' {
s[i] = ' '
break
}
}
}
}
}
return i
}
func prep(r io.Reader) (s []byte, err error) {
buf := &bytes.Buffer{}
_, err = io.Copy(buf, r)
s = buf.Bytes()
if err != nil {
return
}
i := 0
for i < len(s) {
switch s[i] {
case '"':
i += 1
for i < len(s) {
if s[i] == '"' {
i += 1
break
} else if s[i] == '\\' {
i += 1
}
i += 1
}
case '/':
i = consumeComment(s, i+1)
case ',':
j := i
for {
i += 1
if i >= len(s) {
break
} else if s[i] == '}' || s[i] == ']' {
s[j] = ' '
break
} else if s[i] == '/' {
i = consumeComment(s, i+1)
} else if !isWS(s[i]) {
break
}
}
default:
i += 1
}
}
return
}
// Read acts as a proxy for the underlying reader and cleans p
// of comments and trailing commas preceeding ] and }
// comments are delimitted by // up until the end the line
func (st *TrimNodeReader) Read(p []byte) (n int, err error) {
if st.br == nil {
var s []byte
if s, err = prep(st.r); err != nil {
return
}
st.br = bytes.NewReader(s)
}
return st.br.Read(p)
}
// NewTrimNodeReader New returns an io.Reader acting as proxy to r
func NewTrimNodeReader(r io.Reader) io.Reader {
return &TrimNodeReader{r: r}
}

10
common/maps/maps.go Normal file
View File

@@ -0,0 +1,10 @@
package maps
func Merge[k comparable, v any](m1 map[k]v, m2 ...map[k]v) map[k]v {
for _, m2v := range m2 {
for k2, v2 := range m2v {
m1[k2] = v2
}
}
return m1
}

18
common/slices/slice.go Normal file
View File

@@ -0,0 +1,18 @@
package slices
func Range[t any](sl []t, handle func(i int, v t) (_break bool)) {
for i := range sl {
b := handle(i, sl[i])
if b {
break
}
}
}
func RangeToNew[old, new any](sl []old, handle func(i int, v old) new) []new {
ns := make([]new, len(sl))
for i := range ns {
ns[i] = handle(i, sl[i])
}
return ns
}

75
common/watcher/http.go Normal file
View File

@@ -0,0 +1,75 @@
package watcher
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"net/http"
"time"
)
type HTTPWatcher struct {
hash [32]byte
url string
interval uint
handler EventHandler
errorHandler ErrorHandler
close chan struct{}
}
func NewHTTPWatcher(url string, interval uint) *HTTPWatcher {
return &HTTPWatcher{
url: url,
interval: interval,
}
}
func (w *HTTPWatcher) handle() error {
rsp, err := http.Get(w.url)
if err != nil {
return fmt.Errorf("request error: %w", err)
}
defer rsp.Body.Close()
b, err := io.ReadAll(rsp.Body)
if err != nil {
return fmt.Errorf("read body error: %w", err)
}
h := sha256.Sum256(b)
if bytes.Equal(w.hash[:], h[:]) {
return nil
}
w.hash = h
err = w.handler(w.url)
if err != nil {
return fmt.Errorf("handle error: %w", err)
}
return nil
}
func (w *HTTPWatcher) SetEventHandler(handler EventHandler) {
w.handler = handler
}
func (w *HTTPWatcher) SetErrorHandler(handler ErrorHandler) {
w.errorHandler = handler
}
func (w *HTTPWatcher) Watch() error {
go func() {
for range time.Tick(time.Duration(w.interval) * time.Second) {
select {
case <-w.close:
return
default:
}
w.errorHandler(w.handle())
}
}()
return nil
}
func (w *HTTPWatcher) Close() error {
close(w.close)
return nil
}

85
common/watcher/local.go Normal file
View File

@@ -0,0 +1,85 @@
package watcher
import (
"fmt"
"path"
"github.com/fsnotify/fsnotify"
)
type LocalWatcher struct {
dir string
filenames []string
handler EventHandler
errorHandler ErrorHandler
watcher *fsnotify.Watcher
close chan struct{}
}
func NewLocalWatcher(dir string, filenames []string) *LocalWatcher {
return &LocalWatcher{
dir: dir,
filenames: filenames,
close: make(chan struct{}),
}
}
func (w *LocalWatcher) SetEventHandler(handler EventHandler) {
w.handler = handler
}
func (w *LocalWatcher) SetErrorHandler(handler ErrorHandler) {
w.errorHandler = handler
}
func (w *LocalWatcher) handle(e fsnotify.Event) error {
if (!e.Has(fsnotify.Write)) && (!e.Has(fsnotify.Create)) {
return nil
}
name := path.Base(e.Name)
file := ""
for _, filename := range w.filenames {
ok, _ := path.Match(filename, name)
if ok {
file = filename
}
}
if len(file) == 0 {
return nil
}
err := w.handler(file)
if err != nil {
return err
}
return nil
}
func (w *LocalWatcher) Watch() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("new watcher error: %s", err)
}
go func() {
defer watcher.Close()
for {
select {
case e := <-watcher.Events:
err := w.handle(e)
if err != nil {
w.errorHandler(err)
}
case err := <-watcher.Errors:
if err != nil {
w.errorHandler(err)
}
case <-w.close:
return
}
}
}()
return watcher.Add(w.dir)
}
func (w *LocalWatcher) Close() error {
close(w.close)
return w.watcher.Close()
}

11
common/watcher/watcher.go Normal file
View File

@@ -0,0 +1,11 @@
package watcher
type EventHandler func(filename string) error
type ErrorHandler func(err error)
type Watcher interface {
SetEventHandler(handler EventHandler)
SetErrorHandler(handler ErrorHandler)
Watch() error
Close() error
}

11
conf/acme.go Normal file
View File

@@ -0,0 +1,11 @@
package conf
type ACME struct {
Name string
Mode string `json:"CertMode"` // file, http, dns
RejectUnknownSni bool `json:"RejectUnknownSni"`
Provider string `json:"Provider"` // alidns, cloudflare, gandi, godaddy....
Email string `json:"Email"`
DNSEnv map[string]string `json:"DNSEnv"`
Storage string `json:"Storage"`
}

7
conf/common.go Normal file
View File

@@ -0,0 +1,7 @@
package conf
import "strings"
func IsHttpUrl(url string) bool {
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
}

75
conf/conf.go Normal file
View File

@@ -0,0 +1,75 @@
package conf
import (
trim "Ratte/common/json"
"Ratte/common/watcher"
"fmt"
"net/http"
"os"
"github.com/goccy/go-json"
)
type Conf struct {
// internal fields
path string
watcherHandle EventHandler
errorHandler ErrorHandler
configWatcher watcher.Watcher
coreDataWatchers map[int]watcher.Watcher
// config fields
Log Log `json:"Log,omitempty"`
Watcher Watcher `json:"Watcher,omitempty"`
Core []Core `json:"Core,omitempty"`
Acme []ACME `json:"Acme,omitempty"`
Panel []Panel `json:"Panel,omitempty"`
Node []Node `json:"Node,omitempty"`
}
func New(path string) *Conf {
return &Conf{
path: path,
Watcher: Watcher{
WatchLocalConfig: true,
WatchRemoteConfig: true,
},
Log: newLog(),
Core: make([]Core, 0),
Acme: make([]ACME, 0),
Panel: make([]Panel, 0),
Node: make([]Node, 0),
}
}
func (c *Conf) Load(data []byte) error {
if len(data) >= 0 {
err := json.Unmarshal(data, c)
if err != nil {
return fmt.Errorf("decode json error: %w", err)
}
return nil
}
if IsHttpUrl(c.path) {
rsp, err := http.Get(c.path)
if err != nil {
return err
}
defer rsp.Body.Close()
err = json.NewDecoder(trim.NewTrimNodeReader(rsp.Body)).Decode(&c)
if err != nil {
return fmt.Errorf("decode json error: %w", err)
}
} else {
f, err := os.Open(c.path)
if err != nil {
return err
}
defer f.Close()
err = json.NewDecoder(trim.NewTrimNodeReader(f)).Decode(&c)
if err != nil {
return fmt.Errorf("decode json error: %w", err)
}
}
return nil
}

50
conf/conf_test.go Normal file
View File

@@ -0,0 +1,50 @@
package conf
import (
"fmt"
"testing"
)
func TestConf_Load_Local(t *testing.T) {
c := New("./config.json5")
err := c.Load(nil)
if err != nil {
t.Error(err)
}
t.Log(c)
}
func TestConf_Load_Remote(t *testing.T) {
c := New("http://127.0.0.1:9000/config.json5")
err := c.Load(nil)
if err != nil {
t.Error(err)
}
}
func TestConf_Watch(t *testing.T) {
c := New("./config.json5")
err := c.Load(nil)
if err != nil {
t.Error(err)
return
}
t.Log(c)
c.SetEventHandler(func(event uint, target ...string) {
switch event {
case ConfigFileChangedEvent:
t.Log("Event:", "ConfigFileChangedEvent", "target:", target)
case CoreDataPathChangedEvent:
t.Log("Event:", "CoreDataPathChangedEvent", "target:", target)
}
})
c.SetErrorHandler(func(err error) {
t.Error(err)
})
err = c.Watch()
if err != nil {
t.Error(err)
}
t.Log("press any key to done.")
fmt.Scan()
}

22
conf/core.go Normal file
View File

@@ -0,0 +1,22 @@
package conf
import "github.com/goccy/go-json"
type Core struct {
Name string `json:"Name,omitempty"`
Path string `json:"Path,omitempty"`
DataPath string `json:"DataPath,omitempty"`
Config json.RawMessage `json:"Config,omitempty"`
}
type _core Core
func (c *Core) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, (*_core)(c))
if err != nil {
return err
}
if len(c.Config) == 0 {
c.Config = data
}
return nil
}

19
conf/log.go Normal file
View File

@@ -0,0 +1,19 @@
package conf
type Log struct {
Level string `json:"Level,omitempty"`
Output string `json:"Output,omitempty"`
MaxBackups int `json:"MaxBackups,omitempty"`
MaxSize int `json:"MaxSize,omitempty"`
MaxAge int `json:"MaxAge,omitempty"`
}
func newLog() Log {
return Log{
Level: "info",
Output: "",
MaxBackups: 3,
MaxSize: 100,
MaxAge: 28,
}
}

111
conf/node.go Normal file
View File

@@ -0,0 +1,111 @@
package conf
import (
"fmt"
"github.com/goccy/go-json"
)
type rawNodeConfig struct {
Name string `json:"Name"`
RemoteRaw json.RawMessage `json:"Remote"`
OptRaw json.RawMessage `json:"Options"`
TriggerRaw json.RawMessage `json:"Trigger"`
}
type Remote struct {
APIHost string `json:"ApiHost"`
NodeID int `json:"NodeID"`
Key string `json:"ApiKey"`
NodeType string `json:"NodeType"`
Timeout int `json:"Timeout"`
}
type Options struct {
Core string `json:"Core"`
Panel string `json:"Panel"`
Acme string `json:"Acme"`
Cert Cert `json:"Cert"`
Expand map[string]interface{} `json:"Other"`
}
type Trigger struct {
PullNodeCron any `json:"PullNodeCron"`
PullUserCron any `json:"PullUserCron"`
ReportUserCron any `json:"ReportUserCron"`
RenewCertCron any `json:"RenewCertCron"`
}
type Cert struct {
Domain string `json:"Domain"`
CertPath string `json:"Cert"`
KeyPath string `json:"Key"`
}
type Node struct {
Name string `json:"Name"`
Remote Remote `json:"-"`
Trigger Trigger `json:"-"`
Options Options `json:"-"`
}
func (n *Node) UnmarshalJSON(data []byte) (err error) {
rn := rawNodeConfig{}
err = json.Unmarshal(data, &rn)
if err != nil {
return err
}
n.Remote = Remote{
APIHost: "http://127.0.0.1",
Timeout: 30,
}
if len(rn.RemoteRaw) > 0 {
err = json.Unmarshal(rn.RemoteRaw, &n.Remote)
if err != nil {
return
}
} else {
err = json.Unmarshal(data, &n.Remote)
if err != nil {
return
}
}
n.Options = Options{}
if len(rn.OptRaw) > 0 {
err = json.Unmarshal(rn.OptRaw, &n.Options)
if err != nil {
return
}
} else {
err = json.Unmarshal(data, &n.Options)
if err != nil {
return
}
}
n.Trigger = Trigger{
PullNodeCron: 60,
PullUserCron: 60,
ReportUserCron: 60,
RenewCertCron: "0 2 * * *",
}
if len(rn.TriggerRaw) > 0 {
err = json.Unmarshal(rn.OptRaw, &n.Trigger)
if err != nil {
return
}
} else {
err = json.Unmarshal(data, &n.Trigger)
if err != nil {
return
}
}
if len(rn.Name) > 0 {
n.Name = rn.Name
} else {
n.Name = fmt.Sprintf("{T:%s;A:%s;I:%d;}",
n.Remote.NodeType,
n.Remote.APIHost,
n.Remote.NodeID)
}
return
}

6
conf/panel.go Normal file
View File

@@ -0,0 +1,6 @@
package conf
type Panel struct {
Name string `json:"name"`
Path string `json:"path"`
}

78
conf/watcher.go Normal file
View File

@@ -0,0 +1,78 @@
package conf
import (
"Ratte/common/watcher"
"errors"
"fmt"
"path"
)
const (
ConfigFileChangedEvent = 0
CoreDataPathChangedEvent = 1
)
type EventHandler func(event uint, target ...string)
type ErrorHandler func(err error)
type Watcher struct {
WatchLocalConfig bool `json:"WatchLocalConfig,omitempty"`
WatchRemoteConfig bool `json:"WatchRemoteConfig,omitempty"`
WatchCoreDataPath bool `json:"WatchCoreDataPath,omitempty"`
RemoteInterval uint `json:"Interval,omitempty"`
}
func (c *Conf) SetEventHandler(w EventHandler) {
c.watcherHandle = w
}
func (c *Conf) SetErrorHandler(w ErrorHandler) {
c.errorHandler = w
}
func (c *Conf) Watch() error {
if c.watcherHandle != nil {
return errors.New("no watch handler")
}
if IsHttpUrl(c.path) {
if c.Watcher.WatchRemoteConfig {
w := watcher.NewHTTPWatcher(c.path, c.Watcher.RemoteInterval)
c.configWatcher = w
}
} else {
if !c.Watcher.WatchLocalConfig {
w := watcher.NewLocalWatcher(path.Dir(c.path), []string{path.Base(c.path)})
c.configWatcher = w
}
}
if c.Watcher.WatchLocalConfig || c.Watcher.WatchRemoteConfig {
c.configWatcher.SetErrorHandler(watcher.ErrorHandler(c.errorHandler))
c.configWatcher.SetEventHandler(func(_ string) error {
c.watcherHandle(ConfigFileChangedEvent)
return nil
})
err := c.configWatcher.Watch()
if err != nil {
return fmt.Errorf("watch config err:%w", err)
}
}
if !c.Watcher.WatchCoreDataPath {
return nil
}
watchers := make(map[int]*watcher.LocalWatcher, len(c.Core))
for i, co := range c.Core {
w := watcher.NewLocalWatcher(co.DataPath, []string{"*"})
w.SetErrorHandler(watcher.ErrorHandler(c.errorHandler))
w.SetEventHandler(func(_ string) error {
c.watcherHandle(CoreDataPathChangedEvent, c.Core[i].Name)
return nil
})
err := w.Watch()
if err != nil {
return fmt.Errorf("watch core %s err:%w", co.Name, err)
}
watchers[i] = w
}
return nil
}

196
go.mod Normal file
View File

@@ -0,0 +1,196 @@
module Ratte
go 1.22.0
toolchain go1.23.1
require (
github.com/Yuzuki616/Ratte-Interface v0.0.0-20240911204230-d6204ff1f3e0
github.com/fsnotify/fsnotify v1.7.0
github.com/go-acme/lego/v4 v4.18.0
github.com/goccy/go-json v0.10.3
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/x-cray/logrus-prefixed-formatter v0.5.2
)
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect
github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/exoscale/egoscale v0.102.3 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.11.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/gofrs/flock v0.10.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/gophercloud/gophercloud v1.12.0 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.28.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.8.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/goinwx v0.10.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.3.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oracle/oci-go-sdk/v65 v65.63.1 // indirect
github.com/ovh/go-ovh v1.5.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.12.0 // indirect
github.com/sacloud/packages-go v0.0.10 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v3 v3.1.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.5 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v3 v3.9.0 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/ratelimit v0.3.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/api v0.172.0 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.63.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/dolthub/maphashlatest => github.com/Yuzuki616/maphash v0.0.0-20240910225326-accabd8aa54a

1045
go.sum Normal file

File diff suppressed because it is too large Load Diff

11
handler/cert.go Normal file
View File

@@ -0,0 +1,11 @@
package handler
import "fmt"
func (h *Handler) RenewCertHandle() error {
err := h.acme.RenewCert(h.Options.Cert.CertPath, h.Options.Cert.KeyPath, h.Options.Cert.Domain)
if err != nil {
return fmt.Errorf("renew cert error: %w", err)
}
return nil
}

48
handler/handler.go Normal file
View File

@@ -0,0 +1,48 @@
package handler
import (
"Ratte/acme"
"Ratte/conf"
"github.com/sirupsen/logrus"
"sync/atomic"
)
import "github.com/Yuzuki616/Ratte-Interface/core"
import "github.com/Yuzuki616/Ratte-Interface/panel"
type Handler struct {
c core.Core
p panel.Panel
nodeName string
acme *acme.Acme
l *logrus.Entry
userList []panel.UserInfo
userHash map[string]struct{}
nodeAdded atomic.Bool
*conf.Options
}
func New(
c core.Core,
p panel.Panel,
nodeName string,
ac *acme.Acme,
l *logrus.Entry,
opts *conf.Options) *Handler {
return &Handler{
c: c,
p: p,
nodeName: nodeName,
userList: make([]panel.UserInfo, 0),
userHash: make(map[string]struct{}),
acme: ac,
l: l,
Options: opts,
}
}
func (h *Handler) Close() error {
if h.nodeAdded.Load() {
return h.c.DelNode(h.nodeName)
}
return nil
}

50
handler/node.go Normal file
View File

@@ -0,0 +1,50 @@
package handler
import (
"Ratte/common/maps"
"fmt"
"github.com/Yuzuki616/Ratte-Interface/core"
"github.com/Yuzuki616/Ratte-Interface/panel"
"github.com/Yuzuki616/Ratte-Interface/params"
)
func (h *Handler) PullNodeHandle(n *panel.NodeInfo) error {
if h.nodeAdded.Load() {
err := h.c.DelNode(h.nodeName)
if err != nil {
return fmt.Errorf("del node error: %w", err)
}
} else {
err := h.acme.CreateCert(h.Cert.CertPath, h.Cert.KeyPath, h.Cert.Domain)
if err != nil {
return fmt.Errorf("create cert error: %w", err)
}
}
err := h.c.AddNode(&core.AddNodeParams{
NodeInfo: core.NodeInfo{
CommonNodeInfo: params.CommonNodeInfo{
Type: n.Type,
VMess: n.VMess,
Shadowsocks: n.Shadowsocks,
Trojan: n.Trojan,
Hysteria: n.Hysteria,
Other: n.Other,
ExpandParams: params.ExpandParams{
OtherOptions: maps.Merge(n.OtherOptions, h.Expand),
CustomData: n.CustomData,
},
},
TlsOptions: core.TlsOptions{
CertPath: h.Cert.CertPath,
KeyPath: h.Cert.KeyPath,
},
},
})
if err != nil {
return fmt.Errorf("add node error: %w", err)
}
if h.nodeAdded.Load() {
h.nodeAdded.Store(true)
}
return nil
}

87
handler/user.go Normal file
View File

@@ -0,0 +1,87 @@
package handler
import (
"Ratte/common/slices"
"fmt"
"github.com/Yuzuki616/Ratte-Interface/core"
"github.com/Yuzuki616/Ratte-Interface/panel"
)
func compareUserList(old, new []panel.UserInfo) (deleted []string, added []panel.UserInfo) {
tmp := map[string]struct{}{}
tmp2 := map[string]struct{}{}
for i := range old {
tmp[old[i].GetHashOrKey()] = struct{}{}
}
l := len(tmp)
for i := range new {
e := new[i].GetHashOrKey()
tmp[e] = struct{}{}
tmp2[e] = struct{}{}
if l != len(tmp) {
added = append(added, new[i])
l++
}
}
tmp = nil
l = len(tmp2)
for i := range old {
tmp2[old[i].GetHashOrKey()] = struct{}{}
if l != len(tmp2) {
deleted = append(deleted, old[i].Name)
l++
}
}
return deleted, added
}
func (h *Handler) PullUserHandle(users []panel.UserInfo) error {
del, add := compareUserList(h.userList, users)
cas := slices.RangeToNew[panel.UserInfo, core.UserInfo](add, func(_ int, v panel.UserInfo) core.UserInfo {
return core.UserInfo(v.UserInfo)
})
err := h.c.AddUsers(&core.AddUsersParams{
NodeName: h.nodeName,
Users: cas,
})
if err != nil {
return fmt.Errorf("add user error: %w", err)
}
h.l.Infof("Added %d users", len(users))
err = h.c.DelUsers(&core.DelUsersParams{
NodeName: h.nodeName,
Users: del,
})
if err != nil {
return fmt.Errorf("del user error: %w", err)
}
h.l.Infof("Deleted %d users", len(users))
h.userList = users
return nil
}
func (h *Handler) ReportUserHandle(id int) error {
var err error
req := &core.GetUserTrafficParams{NodeName: h.nodeName}
var users []panel.UserTrafficInfo
slices.Range(h.userList, func(_ int, v panel.UserInfo) bool {
req.Username = v.Name
rsp := h.c.GetUserTraffic(req)
if rsp.Err != nil {
err = rsp.Err
return true
}
if rsp.Up == 0 && rsp.Down == 0 {
return false
}
return false
})
err = h.p.ReportUserTraffic(&panel.ReportUserTrafficParams{
Id: id,
Users: users,
})
if err != nil {
return fmt.Errorf("report user error: %w", err)
}
return nil
}

1
log/log.go Normal file
View File

@@ -0,0 +1 @@
package log

103
trigger/handle.go Normal file
View File

@@ -0,0 +1,103 @@
package trigger
import (
"fmt"
"github.com/robfig/cron/v3"
)
func (t *Trigger) addCronHandle(cron any, job cron.FuncJob) (cron.EntryID, error) {
switch cron.(type) {
case string:
return t.c.AddJob(cron.(string), job)
case int:
return t.c.Schedule(newSchedule(cron.(int)), job), nil
default:
return 0, fmt.Errorf("unknown cron type: %T", cron)
}
}
func (t *Trigger) hashEqualsOrStore(name, hash string) bool {
if h, ok := t.hashs[name]; ok {
if h == hash {
return true
}
t.hashs[name] = hash
} else {
t.hashs[name] = hash
}
return false
}
func (t *Trigger) pullNodeHandle() {
t.l.Info("Run pull node task...")
defer t.l.Info("Run pull node task done.")
// get node info
nn := t.p.GetNodeInfo(t.remoteId)
if nn.Err != nil {
t.l.WithError(nn.Err).Error("Get node info failed")
return
}
if t.hashEqualsOrStore("pullNode", nn.GetHash()) {
t.l.Debug("Node is not changed, skip")
return
}
t.l.Debug("Node is changed, triggering handler...")
// update node handler
err := t.h.PullNodeHandle(&nn.NodeInfo)
if err != nil {
t.l.WithError(err).Error("Pull node failed")
return
}
// done
t.l.Debug("trigger handler done.")
}
func (t *Trigger) pullUserHandle() {
t.l.Info("Run pull user task...")
defer t.l.Info("Run pull user task done.")
// get user info
nu := t.p.GetUserList(t.remoteId)
if nu.Err != nil {
t.l.WithError(nu.Err).Error("Get user list failed")
return
}
if t.hashEqualsOrStore("pullUser", nu.GetHash()) {
t.l.Debug("Node is not changed, skip")
return
}
t.l.Debug("user list is changed, triggering handler...")
// triggering update user list handler
err := t.h.PullUserHandle(nu.Users)
if err != nil {
t.l.WithError(err).Error("Pull user handle failed")
return
}
// done
t.l.Debug("trigger handler done.")
}
func (t *Trigger) reportUserHandle() {
t.l.Info("Run report user task...")
defer t.l.Info("Run pull user task done.")
// triggering report user handler
err := t.h.ReportUserHandle()
if err != nil {
t.l.WithError(err).Error("Report user handle failed")
return
}
// done
}
func (t *Trigger) renewCertCron() {
t.l.Info("Run renew cert task...")
defer t.l.Info("Run renew cert task done.")
// triggering renew cert handler
err := t.h.RenewCertHandle()
if err != nil {
t.l.WithError(err).Error("Renew cert handle failed")
return
}
// done
}

17
trigger/schedule.go Normal file
View File

@@ -0,0 +1,17 @@
package trigger
import "time"
type Schedule struct {
interval int
}
func newSchedule(interval int) *Schedule {
return &Schedule{
interval: interval,
}
}
func (s *Schedule) Next(t time.Time) time.Time {
return t.Add(time.Duration(s.interval) * time.Second)
}

88
trigger/trigger.go Normal file
View File

@@ -0,0 +1,88 @@
package trigger
import (
"Ratte/conf"
"Ratte/handler"
"fmt"
"github.com/Yuzuki616/Ratte-Interface/panel"
"github.com/robfig/cron/v3"
"github.com/sirupsen/logrus"
)
type Trigger struct {
l *logrus.Entry
c *cron.Cron
h *handler.Handler
p panel.Panel
remoteId int
remoteC *conf.Remote
hashs map[string]string
}
func NewTrigger(
l *logrus.Entry,
tc *conf.Trigger,
h *handler.Handler,
p panel.Panel,
rm *conf.Remote,
) (*Trigger, error) {
tr := &Trigger{
l: l,
c: cron.New(),
h: h,
p: p,
remoteC: rm,
}
// add pull node cron task
_, err := tr.addCronHandle(tc.PullNodeCron, tr.pullNodeHandle)
if err != nil {
return nil, err
}
// add pull user cron task
_, err = tr.addCronHandle(tc.PullUserCron, tr.pullUserHandle)
if err != nil {
return nil, err
}
// add report user cron task
_, err = tr.addCronHandle(tc.ReportUserCron, tr.reportUserHandle)
if err != nil {
return nil, err
}
// add renew cert cron task
_, err = tr.addCronHandle(tc.RenewCertCron, tr.renewCertCron)
if err != nil {
return nil, err
}
return tr, nil
}
func (t *Trigger) Start() error {
r := t.remoteC
rsp := t.p.AddRemote(&panel.AddRemoteParams{
Baseurl: r.APIHost,
NodeId: r.NodeID,
NodeType: r.NodeType,
Timeout: r.Timeout,
})
if rsp.Err != nil {
return rsp.Err
}
t.remoteId = rsp.RemoteId
t.pullNodeHandle()
t.pullUserHandle()
t.c.Start()
return nil
}
func (t *Trigger) Close() error {
t.c.Stop()
err := t.p.DelRemote(t.remoteId)
if err != nil {
return fmt.Errorf("del remote err: %w", err)
}
return nil
}