Подготовить HY2XS к production-сборке

This commit is contained in:
2026-04-25 23:13:12 +05:00
commit 84a4e94567
277 changed files with 26513 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
package proxy
import (
"errors"
"github.com/sirupsen/logrus"
"hy2xs-admin/model/constant"
"hy2xs-admin/util"
"os/exec"
"sync"
)
type Hysteria2Process struct {
process
binPath string
configPath string
}
var mutexHysteria2 sync.Mutex
var cmdHysteria2 exec.Cmd
var hysteria2Instance *Hysteria2Process
func init() {
hysteria2Instance = &Hysteria2Process{process{mutex: &mutexHysteria2, cmd: &cmdHysteria2}, util.GetHysteria2BinPath(), constant.Hysteria2ConfigPath}
}
func NewHysteria2Instance() *Hysteria2Process {
return hysteria2Instance
}
func (h *Hysteria2Process) IsRunning() bool {
return h.isRunning()
}
func (h *Hysteria2Process) StartHysteria2() error {
if err := h.start(h.binPath, "-c", h.configPath, "server"); err != nil {
_ = util.RemoveFile(h.configPath)
logrus.Errorf("start hysteria2 err: %v", err)
return errors.New("start hysteria2 err")
}
return nil
}
func (h *Hysteria2Process) StopHysteria2() error {
if err := h.stop(); err != nil {
logrus.Errorf("stop hysteria2 err: %v", err)
return errors.New("stop hysteria2 err")
}
_ = util.RemoveFile(h.configPath)
return nil
}
func (h *Hysteria2Process) Release() error {
if err := h.release(); err != nil {
logrus.Errorf("release hysteria2 err: %v", err)
return errors.New("release hysteria2 err")
}
return nil
}
+136
View File
@@ -0,0 +1,136 @@
package proxy
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"io"
"net/http"
"time"
)
type Hysteria2Api struct {
apiPort int64
}
func NewHysteria2Api(apiPort int64) *Hysteria2Api {
return &Hysteria2Api{
apiPort: apiPort,
}
}
// ListUsers 每个用户的流量信息
func (h *Hysteria2Api) ListUsers(clear bool, secret string) (map[string]bo.Hysteria2UserTraffic, error) {
var users map[string]bo.Hysteria2UserTraffic
if !NewHysteria2Instance().IsRunning() {
return users, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
url := fmt.Sprintf("http://127.0.0.1:%d/traffic", h.apiPort)
if clear {
url = fmt.Sprintf("%s?clear=1", url)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
logrus.Errorf("Hysteria2 ListUsers NewRequest err: %v", err)
return nil, errors.New(constant.SysError)
}
req.Header.Set("Authorization", secret)
resp, err := http.DefaultClient.Do(req)
defer func() {
if resp != nil {
_ = resp.Body.Close()
}
}()
if err != nil || resp.StatusCode != http.StatusOK {
logrus.Errorf("Hysteria2 ListUsers err: %v", err)
return nil, errors.New("http connection error")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
logrus.Errorf("Hysteria2 io read err: %v", err)
return nil, errors.New("http connection error")
}
if err = json.Unmarshal(body, &users); err != nil {
logrus.Errorf("Hysteria2 ListUsers Unmarshal err: %v", err)
return nil, errors.New(constant.SysError)
}
return users, nil
}
// KickUsers 踢下线
func (h *Hysteria2Api) KickUsers(keys []string, secret string) error {
if !NewHysteria2Instance().IsRunning() {
return nil
}
usernamesByte, err := json.Marshal(keys)
if err != nil {
logrus.Errorf("Hysteria2 KickUsers Marshal err: %v", err)
return errors.New(constant.SysError)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
url := fmt.Sprintf("http://127.0.0.1:%d/kick", h.apiPort)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url,
bytes.NewBuffer(usernamesByte))
if err != nil {
logrus.Errorf("Hysteria2 KickUsers NewRequest err: %v", err)
return errors.New(constant.SysError)
}
req.Header.Set("Authorization", secret)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
if err != nil || resp.StatusCode != http.StatusOK {
logrus.Errorf("Hysteria2 KickUsers err: %v", err)
return errors.New("http connection error")
}
return nil
}
// OnlineUsers 在线用户
func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) {
var onlineUsers map[string]int64
if !NewHysteria2Instance().IsRunning() {
return onlineUsers, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
url := fmt.Sprintf("http://127.0.0.1:%d/online", h.apiPort)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
logrus.Errorf("Hysteria2 OnlineUsers NewRequest err: %v", err)
return nil, errors.New(constant.SysError)
}
req.Header.Set("Authorization", secret)
resp, err := http.DefaultClient.Do(req)
defer func() {
if resp != nil {
_ = resp.Body.Close()
}
}()
if err != nil || resp.StatusCode != http.StatusOK {
logrus.Errorf("Hysteria2 OnlineUsers err: %v", err)
return nil, errors.New("http connection error")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
logrus.Errorf("Hysteria2 io read err: %v", err)
return nil, errors.New("http connection error")
}
if err = json.Unmarshal(body, &onlineUsers); err != nil {
logrus.Errorf("Hysteria2 OnlineUsers Unmarshal err: %v", err)
return nil, errors.New(constant.SysError)
}
return onlineUsers, nil
}
+229
View File
@@ -0,0 +1,229 @@
package proxy
import (
"bufio"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"hy2xs-admin/model/constant"
"io"
"os/exec"
"sync"
"syscall"
"time"
)
var logger logrus.Logger
func initLogger() {
logger.SetOutput(&lumberjack.Logger{
Filename: constant.Hysteria2LogPath,
MaxSize: 1,
MaxBackups: 2,
MaxAge: 30,
Compress: true,
LocalTime: true,
})
logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})
logger.SetLevel(logrus.InfoLevel)
}
func init() {
initLogger()
}
type process struct {
mutex *sync.Mutex
cmd *exec.Cmd
}
func (p *process) isRunning() bool {
return p.cmd != nil && p.cmd.Process != nil && p.cmd.ProcessState == nil
}
func (p *process) start(name string, arg ...string) error {
if !p.mutex.TryLock() {
logrus.Errorf("start cmd err: lock not acquired")
return errors.New("start cmd err")
}
defer p.mutex.Unlock()
if p.isRunning() {
return nil
}
cmd := exec.Command(name, arg...)
if cmd.Err != nil {
logrus.Errorf("cmd err: %v", cmd.Err)
return errors.New("cmd err")
}
// 获取命令的 stdout 和 stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
logrus.Errorf("Error obtaining stdout: %v", err)
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
logrus.Errorf("Error obtaining stderr: %v", err)
return err
}
if err := cmd.Start(); err != nil {
logrus.Errorf("cmd start err: %v", err)
return errors.New("cmd start err")
}
p.cmd = cmd
go p.handleLogs(stdout, stderr)
return nil
}
func (p *process) stop() error {
if !p.mutex.TryLock() {
return errors.New("cmd stop err: lock not acquired")
}
if !p.isRunning() {
p.mutex.Unlock()
return nil
}
cmd := p.cmd
p.mutex.Unlock()
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
logrus.Warnf("send SIGTERM failed: %v", err)
}
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
select {
case err := <-done:
if normalizeExitErr(err, syscall.SIGTERM) != nil {
return fmt.Errorf("process exit failed: %w", err)
}
case <-timer.C:
if err := cmd.Process.Kill(); err != nil {
return fmt.Errorf("SIGKILL failed: %w", err)
}
err := <-done
if normalizeExitErr(err, syscall.SIGKILL) != nil {
return fmt.Errorf("process killed but exit abnormal: %w", err)
}
}
p.mutex.Lock()
p.cmd = nil
p.mutex.Unlock()
return nil
}
func normalizeExitErr(err error, allowedSignals ...syscall.Signal) error {
if err == nil {
return nil
}
exitErr, ok := err.(*exec.ExitError)
if !ok {
return err
}
status, ok := exitErr.Sys().(syscall.WaitStatus)
if !ok {
return err
}
if status.Signaled() {
sig := status.Signal()
for _, s := range allowedSignals {
if sig == s {
return nil
}
}
}
return err
}
func (p *process) release() error {
if !p.mutex.TryLock() {
logrus.Errorf("cmd release err: lock not acquired")
return errors.New("cmd release err")
}
defer p.mutex.Unlock()
if !p.isRunning() {
return nil
}
if err := p.cmd.Process.Release(); err != nil {
logrus.Errorf("cmd release err: %v", err)
return errors.New("cmd release err")
}
p.cmd = nil
return nil
}
func (p *process) handleLogs(stdout, stderr io.ReadCloser) {
// 日志
stdoutChan := make(chan string)
stderrChan := make(chan string)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
stdoutChan <- scanner.Text()
}
if err := scanner.Err(); err != nil {
logrus.Errorf("Error reading stdout: %v", err)
}
close(stdoutChan)
}()
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
stderrChan <- scanner.Text()
}
if err := scanner.Err(); err != nil {
logrus.Errorf("Error reading stderr: %v", err)
}
close(stderrChan)
}()
for {
select {
case line, ok := <-stdoutChan:
if !ok {
stdoutChan = nil
} else {
logger.Infof(line)
}
case line, ok := <-stderrChan:
if !ok {
stderrChan = nil
} else {
logger.Errorf(line)
}
}
// 当两个 channel 都关闭时,退出循环
if stdoutChan == nil && stderrChan == nil {
break
}
}
}