refactor global and admin service to use grpc

This commit is contained in:
Hiddify
2024-03-16 03:01:22 +01:00
parent 361419b95e
commit 6039baa313
20 changed files with 553 additions and 1337 deletions

View File

@@ -8,6 +8,7 @@ import (
pb "github.com/hiddify/libcore/hiddifyrpc"
)
var EnableBridge = true
var coreInfoObserver = NewObserver[pb.CoreInfoResponse](10)
var CoreState = pb.CoreState_STOPPED
@@ -20,8 +21,10 @@ func SetCoreStatus(state pb.CoreState, msgType pb.MessageType, message string) p
Message: message,
}
coreInfoObserver.Emit(info)
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState)})
bridge.SendStringToPort(statusPropagationPort, string(msg))
if EnableBridge {
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState)})
bridge.SendStringToPort(statusPropagationPort, string(msg))
}
return info
}

View File

@@ -13,7 +13,6 @@ import (
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
var Box *libbox.BoxService
@@ -34,9 +33,11 @@ func StopAndAlert(msgType pb.MessageType, message string) {
if commandServer != nil {
commandServer.Close()
}
alert := msgType.String()
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState), Alert: &alert, Message: &message})
bridge.SendStringToPort(statusPropagationPort, string(msg))
if EnableBridge {
alert := msgType.String()
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState), Alert: &alert, Message: &message})
bridge.SendStringToPort(statusPropagationPort, string(msg))
}
}
func (s *CoreService) Start(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
@@ -48,15 +49,17 @@ func Start(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting")
if CoreState != pb.CoreState_STOPPED {
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting0000")
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_STOPPED,
}, fmt.Errorf("instance not stopped")
}
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting1111")
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "Starting222")
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting2")
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
resp, err := StartService(in)
return resp, err
@@ -65,7 +68,7 @@ func (s *CoreService) StartService(ctx context.Context, in *pb.StartRequest) (*p
return StartService(in)
}
func StartService(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting3")
content := in.ConfigContent
if content == "" {
@@ -80,22 +83,28 @@ func StartService(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
}
content = string(fileContent)
}
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting4")
Log(pb.LogLevel_INFO, pb.LogType_CORE, content)
parsedContent, err := parseConfig(content)
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Parsed")
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_PARSING_CONFIG, err.Error())
return &resp, err
}
var patchedOptions *option.Options
patchedOptions, err = config.BuildConfig(*configOptions, parsedContent)
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_BUILDING_CONFIG, err.Error())
return &resp, err
if !in.EnableRawConfig {
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Building config")
parsedContent_tmp, err := config.BuildConfig(*configOptions, parsedContent)
parsedContent = *parsedContent_tmp
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_BUILDING_CONFIG, err.Error())
return &resp, err
}
}
config.SaveCurrentConfig(filepath.Join(sWorkingPath, "current-config.json"), *patchedOptions)
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Saving Contnet")
config.SaveCurrentConfig(filepath.Join(sWorkingPath, "current-config.json"), parsedContent)
if in.EnableOldCommandServer {
err = startCommandServer(*logFactory)
if err != nil {
@@ -105,13 +114,15 @@ func StartService(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
}
}
instance, err := NewService(*patchedOptions)
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Stating Service ")
instance, err := NewService(parsedContent)
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_CREATE_SERVICE, err.Error())
return &resp, err
}
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Service.. started")
if in.DelayStart {
<-time.After(250 * time.Millisecond)
}

233
v2/standalone.go Normal file
View File

@@ -0,0 +1,233 @@
package v2
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/hiddify/libcore/config"
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing-box/option"
)
func RunStandalone(hiddifySettingPath string, configPath string) error {
fmt.Println("Running in standalone mode")
current, err := readAndBuildConfig(hiddifySettingPath, configPath)
if err != nil {
fmt.Printf("Error in read and build config %v", err)
return err
}
go StartService(&pb.StartRequest{
ConfigPath: configPath,
EnableOldCommandServer: false,
DelayStart: false,
})
go updateConfigInterval(current, hiddifySettingPath, configPath)
fmt.Printf("Press CTRL+C to stop\n")
fmt.Printf("Open http://localhost:6756/?secret=hiddify in your browser\n")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
_, err = Stop()
return err
}
type ConfigResult struct {
Config string
RefreshInterval int
}
func readAndBuildConfig(hiddifySettingPath string, configPath string) (ConfigResult, error) {
var result ConfigResult
result, err := readConfigContent(configPath)
if err != nil {
return result, err
}
hiddifyconfig := config.DefaultConfigOptions()
if hiddifySettingPath != "" {
hiddifyconfig, err = readConfigOptionsAt(hiddifySettingPath)
if err != nil {
return result, err
}
}
result.Config, err = buildConfig(result.Config, *hiddifyconfig)
if err != nil {
return result, err
}
return result, nil
}
func readConfigContent(configPath string) (ConfigResult, error) {
var content string
var refreshInterval int
if strings.HasPrefix(configPath, "http://") || strings.HasPrefix(configPath, "https://") {
client := &http.Client{}
// Create a new request
req, err := http.NewRequest("GET", configPath, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return ConfigResult{}, err
}
req.Header.Set("User-Agent", "HiddifyNext/17.5.0 ("+runtime.GOOS+") like ClashMeta v2ray sing-box")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making GET request:", err)
return ConfigResult{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ConfigResult{}, fmt.Errorf("failed to read config body: %w", err)
}
content = string(body)
refreshInterval, _ = extractRefreshInterval(resp.Header, content)
fmt.Printf("Refresh interval: %d\n", refreshInterval)
} else {
data, err := ioutil.ReadFile(configPath)
if err != nil {
return ConfigResult{}, fmt.Errorf("failed to read config file: %w", err)
}
content = string(data)
}
return ConfigResult{
Config: content,
RefreshInterval: refreshInterval,
}, nil
}
func extractRefreshInterval(header http.Header, bodyStr string) (int, error) {
refreshIntervalStr := header.Get("profile-update-interval")
if refreshIntervalStr != "" {
refreshInterval, err := strconv.Atoi(refreshIntervalStr)
if err != nil {
return 0, fmt.Errorf("failed to parse refresh interval from header: %w", err)
}
return refreshInterval, nil
}
lines := strings.Split(bodyStr, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "//profile-update-interval:") || strings.HasPrefix(line, "#profile-update-interval:") {
parts := strings.SplitN(line, ":", 2)
str := strings.TrimSpace(parts[1])
refreshInterval, err := strconv.Atoi(str)
if err != nil {
return 0, fmt.Errorf("failed to parse refresh interval from body: %w", err)
}
return refreshInterval, nil
}
}
return 0, nil
}
func buildConfig(configContent string, options config.ConfigOptions) (string, error) {
parsedContent, err := config.ParseConfigContent(configContent, true)
if err != nil {
return "", fmt.Errorf("failed to parse config content: %w", err)
}
singconfigs, err := readConfigBytes([]byte(parsedContent))
if err != nil {
return "", err
}
finalconfig, err := config.BuildConfig(options, *singconfigs)
if err != nil {
return "", fmt.Errorf("failed to build config: %w", err)
}
finalconfig.Log.Output = ""
finalconfig.Experimental.ClashAPI.ExternalUI = "webui"
if options.AllowConnectionFromLAN {
finalconfig.Experimental.ClashAPI.ExternalController = "0.0.0.0:6756"
} else {
finalconfig.Experimental.ClashAPI.ExternalController = "127.0.0.1:6756"
}
if finalconfig.Experimental.ClashAPI.Secret == "" {
// finalconfig.Experimental.ClashAPI.Secret = "hiddify"
}
if err := Setup("./", "./", "./tmp", 0, false); err != nil {
return "", fmt.Errorf("failed to set up global configuration: %w", err)
}
configStr, err := config.ToJson(*finalconfig)
if err != nil {
return "", fmt.Errorf("failed to convert config to JSON: %w", err)
}
return configStr, nil
}
func updateConfigInterval(current ConfigResult, hiddifySettingPath string, configPath string) {
if current.RefreshInterval <= 0 {
return
}
for {
<-time.After(time.Duration(current.RefreshInterval) * time.Hour)
new, err := readAndBuildConfig(hiddifySettingPath, configPath)
if err != nil {
continue
}
if new.Config != current.Config {
go Stop()
go StartService(&pb.StartRequest{
ConfigContent: new.Config,
DelayStart: false,
EnableOldCommandServer: false,
DisableMemoryLimit: false,
})
}
current = new
}
}
func readConfigBytes(content []byte) (*option.Options, error) {
var options option.Options
err := options.UnmarshalJSON(content)
if err != nil {
return nil, err
}
return &options, nil
}
func readConfigOptionsAt(path string) (*config.ConfigOptions, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var options config.ConfigOptions
err = json.Unmarshal(content, &options)
if err != nil {
return nil, err
}
if options.Warp.WireguardConfigStr != "" {
err := json.Unmarshal([]byte(options.Warp.WireguardConfigStr), &options.Warp.WireguardConfig)
if err != nil {
return nil, err
}
}
return &options, nil
}

View File

@@ -0,0 +1,146 @@
package v2
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/kardianos/service"
)
var logger service.Logger
type hiddifyNext struct{}
var port int = 18020
func (m *hiddifyNext) Start(s service.Service) error {
go StartTunnelGrpcServer(fmt.Sprintf("127.0.0.1:%d", port))
return nil
}
func (m *hiddifyNext) Stop(s service.Service) error {
_, err := Stop()
if err != nil {
return nil
}
// Stop should not block. Return with a few seconds.
// <-time.After(time.Second * 1)
return nil
}
func getCurrentExecutableDirectory() string {
executablePath, err := os.Executable()
if err != nil {
return ""
}
// Extract the directory (folder) containing the executable
executableDirectory := filepath.Dir(executablePath)
return executableDirectory
}
func StartTunnelService(goArg string) (int, string) {
svcConfig := &service.Config{
Name: "HiddifyTunnelService",
DisplayName: "Hiddify Tunnel Service",
Arguments: []string{"tunnel", "run"},
Description: "This is a bridge for tunnel",
Option: map[string]interface{}{
"RunAtLoad": true,
"WorkingDirectory": getCurrentExecutableDirectory(),
},
}
prg := &hiddifyNext{}
s, err := service.New(prg, svcConfig)
if err != nil {
// log.Printf("Error: %v", err)
return 1, fmt.Sprintf("Error: %v", err)
}
if len(goArg) > 0 && goArg != "run" {
return control(s, goArg)
}
logger, err = s.Logger(nil)
if err != nil {
log.Printf("Error: %v", err)
}
err = s.Run()
if err != nil {
logger.Error(err)
return 3, fmt.Sprintf("Error: %v", err)
}
return 0, ""
}
func control(s service.Service, goArg string) (int, string) {
dolog := false
var err error
status, serr := s.Status()
if dolog {
fmt.Printf("Current Status: %+v %+v!\n", status, serr)
}
switch goArg {
case "uninstall":
if status == service.StatusRunning {
s.Stop()
}
if dolog {
fmt.Printf("Tunnel Service Uninstalled Successfully.\n")
}
err = s.Uninstall()
case "start":
if status == service.StatusRunning {
if dolog {
fmt.Printf("Tunnel Service Already Running.\n")
}
return 0, "Tunnel Service Already Running."
} else if status == service.StatusUnknown {
s.Uninstall()
s.Install()
status, serr = s.Status()
if dolog {
fmt.Printf("Check status again: %+v %+v!", status, serr)
}
}
if status != service.StatusRunning {
err = s.Start()
}
case "install":
s.Uninstall()
err = s.Install()
status, serr = s.Status()
if dolog {
fmt.Printf("Check Status Again: %+v %+v", status, serr)
}
if status != service.StatusRunning {
err = s.Start()
}
case "stop":
if status == service.StatusStopped {
if dolog {
fmt.Printf("Tunnel Service Already Stopped.\n")
}
return 0, "Tunnel Service Already Stopped."
}
err = s.Stop()
default:
err = service.Control(s, goArg)
}
if err == nil {
out := fmt.Sprintf("Tunnel Service %sed Successfully.", goArg)
if dolog {
fmt.Printf(out)
}
return 0, out
} else {
out := fmt.Sprintf("Error: %v", err)
if dolog {
log.Printf(out)
}
return 2, out
}
}

92
v2/tunnel_service.go Normal file
View File

@@ -0,0 +1,92 @@
package v2
import (
"context"
"fmt"
"os"
pb "github.com/hiddify/libcore/hiddifyrpc"
)
func (s *TunnelService) Start(ctx context.Context, in *pb.TunnelStartRequest) (*pb.TunnelResponse, error) {
if in.ServerPort == 0 {
in.ServerPort = 2334
}
EnableBridge = false
res, err := Start(&pb.StartRequest{
ConfigContent: makeTunnelConfig(in.Ipv6, in.ServerPort, in.StrictRoute, in.EndpointIndependentNat, in.Stack),
EnableOldCommandServer: false,
DisableMemoryLimit: false,
EnableRawConfig: true,
})
fmt.Printf("Start Result: %+v\n", res)
if err != nil {
return &pb.TunnelResponse{
Message: err.Error(),
}, err
}
return &pb.TunnelResponse{
Message: "OK",
}, err
}
func makeTunnelConfig(Ipv6 bool, ServerPort int32, StrictRoute bool, EndpointIndependentNat bool, Stack string) string {
var ipv6 string
if Ipv6 {
ipv6 = ` "inet6_address": "fdfe:dcba:9876::1/126",`
} else {
ipv6 = ""
}
base := `{
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "HiddifyTunnel",
"inet4_address": "172.19.0.1/30",
` + ipv6 + `
"mtu": 9000,
"auto_route": true,
"strict_route": ` + fmt.Sprintf("%t", StrictRoute) + `,
"endpoint_independent_nat": ` + fmt.Sprintf("%t", EndpointIndependentNat) + `,
"stack": "` + Stack + `"
}
],
"outbounds": [
{
"type": "socks",
"tag": "socks-out",
"server": "127.0.0.1",
"server_port": ` + fmt.Sprintf("%d", ServerPort) + `,
"version": "5"
}
]
}`
return base
}
func (s *TunnelService) Stop(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
_, err := Stop()
if err != nil {
return &pb.TunnelResponse{
Message: err.Error(),
}, err
}
return &pb.TunnelResponse{
Message: "OK",
}, err
}
func (s *TunnelService) Status(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
return &pb.TunnelResponse{
Message: "Not Implemented",
}, nil
}
func (s *TunnelService) Exit(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
Stop()
os.Exit(0)
return &pb.TunnelResponse{
Message: "OK",
}, nil
}