新增调试信息
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
type CommandClientHandler struct {
|
||||
port int64
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) Connected() {
|
||||
cch.logger.Debug("CONNECTED")
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) Disconnected(message string) {
|
||||
cch.logger.Debug("DISCONNECTED: ", message)
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) ClearLog() {
|
||||
cch.logger.Debug("clear log")
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) WriteLog(message string) {
|
||||
cch.logger.Debug("log: ", message)
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) WriteStatus(message *libbox.StatusMessage) {
|
||||
systemInfoObserver.Emit(pb.SystemInfo{
|
||||
ConnectionsIn: message.ConnectionsIn,
|
||||
ConnectionsOut: message.ConnectionsOut,
|
||||
Uplink: message.Uplink,
|
||||
Downlink: message.Downlink,
|
||||
UplinkTotal: message.UplinkTotal,
|
||||
DownlinkTotal: message.DownlinkTotal,
|
||||
Memory: message.Memory,
|
||||
Goroutines: message.Goroutines,
|
||||
})
|
||||
cch.logger.Debug("Memory: ", libbox.FormatBytes(message.Memory), ", Goroutines: ", message.Goroutines)
|
||||
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) WriteGroups(message libbox.OutboundGroupIterator) {
|
||||
if message == nil {
|
||||
return
|
||||
}
|
||||
groups := pb.OutboundGroupList{}
|
||||
for message.HasNext() {
|
||||
group := message.Next()
|
||||
items := group.GetItems()
|
||||
groupItems := []*pb.OutboundGroupItem{}
|
||||
for items.HasNext() {
|
||||
item := items.Next()
|
||||
groupItems = append(groupItems,
|
||||
&pb.OutboundGroupItem{
|
||||
Tag: item.Tag,
|
||||
Type: item.Type,
|
||||
UrlTestTime: item.URLTestTime,
|
||||
UrlTestDelay: item.URLTestDelay,
|
||||
},
|
||||
)
|
||||
}
|
||||
groups.Items = append(groups.Items, &pb.OutboundGroup{Tag: group.Tag, Type: group.Type, Selected: group.Selected, Items: groupItems})
|
||||
}
|
||||
outboundsInfoObserver.Emit(groups)
|
||||
mainOutboundsInfoObserver.Emit(groups)
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) InitializeClashMode(modeList libbox.StringIterator, currentMode string) {
|
||||
cch.logger.Debug("initial clash mode: ", currentMode)
|
||||
}
|
||||
|
||||
func (cch *CommandClientHandler) UpdateClashMode(newMode string) {
|
||||
cch.logger.Debug("update clash mode: ", newMode)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
systemInfoObserver = NewObserver[pb.SystemInfo](10)
|
||||
outboundsInfoObserver = NewObserver[pb.OutboundGroupList](10)
|
||||
mainOutboundsInfoObserver = NewObserver[pb.OutboundGroupList](10)
|
||||
)
|
||||
|
||||
var (
|
||||
statusClient *libbox.CommandClient
|
||||
groupClient *libbox.CommandClient
|
||||
groupInfoOnlyClient *libbox.CommandClient
|
||||
)
|
||||
|
||||
func (s *CoreService) GetSystemInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.SystemInfo]) error {
|
||||
if statusClient == nil {
|
||||
statusClient = libbox.NewCommandClient(
|
||||
&CommandClientHandler{},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandStatus,
|
||||
StatusInterval: 1000000000, // 1000ms debounce
|
||||
},
|
||||
)
|
||||
|
||||
defer func() {
|
||||
statusClient.Disconnect()
|
||||
statusClient = nil
|
||||
}()
|
||||
statusClient.Connect()
|
||||
}
|
||||
|
||||
sub, done, _ := systemInfoObserver.Subscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-done:
|
||||
return nil
|
||||
case info := <-sub:
|
||||
stream.Send(&info)
|
||||
case <-time.After(1000 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CoreService) OutboundsInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.OutboundGroupList]) error {
|
||||
if groupClient == nil {
|
||||
groupClient = libbox.NewCommandClient(
|
||||
&CommandClientHandler{},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandGroup,
|
||||
StatusInterval: 500000000, // 500ms debounce
|
||||
},
|
||||
)
|
||||
|
||||
defer func() {
|
||||
groupClient.Disconnect()
|
||||
groupClient = nil
|
||||
}()
|
||||
groupClient.Connect()
|
||||
}
|
||||
|
||||
sub, done, _ := outboundsInfoObserver.Subscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-done:
|
||||
return nil
|
||||
case info := <-sub:
|
||||
stream.Send(&info)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CoreService) MainOutboundsInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.OutboundGroupList]) error {
|
||||
if groupInfoOnlyClient == nil {
|
||||
groupInfoOnlyClient = libbox.NewCommandClient(
|
||||
&CommandClientHandler{},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandGroupInfoOnly,
|
||||
StatusInterval: 500000000, // 500ms debounce
|
||||
},
|
||||
)
|
||||
|
||||
defer func() {
|
||||
groupInfoOnlyClient.Disconnect()
|
||||
groupInfoOnlyClient = nil
|
||||
}()
|
||||
groupInfoOnlyClient.Connect()
|
||||
}
|
||||
|
||||
sub, stopch, _ := mainOutboundsInfoObserver.Subscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-stopch:
|
||||
return nil
|
||||
case info := <-sub:
|
||||
stream.Send(&info)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CoreService) SelectOutbound(ctx context.Context, in *pb.SelectOutboundRequest) (*pb.Response, error) {
|
||||
return SelectOutbound(in)
|
||||
}
|
||||
|
||||
func SelectOutbound(in *pb.SelectOutboundRequest) (*pb.Response, error) {
|
||||
err := libbox.NewStandaloneCommandClient().SelectOutbound(in.GroupTag, in.OutboundTag)
|
||||
if err != nil {
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_OK,
|
||||
Message: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) UrlTest(ctx context.Context, in *pb.UrlTestRequest) (*pb.Response, error) {
|
||||
return UrlTest(in)
|
||||
}
|
||||
|
||||
func UrlTest(in *pb.UrlTestRequest) (*pb.Response, error) {
|
||||
err := libbox.NewStandaloneCommandClient().URLTest(in.GroupTag)
|
||||
if err != nil {
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_OK,
|
||||
Message: "",
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CanConnectIPv6Addr(remoteAddr netip.AddrPort) bool {
|
||||
dialer := net.Dialer{
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp6", remoteAddr.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func CanConnectIPv6() bool {
|
||||
return CanConnectIPv6Addr(netip.MustParseAddrPort("[2001:4860:4860::8888]:80"))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hiddify/hiddify-core/bridge"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
coreInfoObserver = *NewObserver[*pb.CoreInfoResponse](1)
|
||||
CoreState = pb.CoreState_STOPPED
|
||||
)
|
||||
|
||||
func SetCoreStatus(state pb.CoreState, msgType pb.MessageType, message string) *pb.CoreInfoResponse {
|
||||
msg := fmt.Sprintf("%s: %s %s", state.String(), msgType.String(), message)
|
||||
if msgType == pb.MessageType_EMPTY {
|
||||
msg = fmt.Sprintf("%s: %s", state.String(), message)
|
||||
}
|
||||
Log(pb.LogLevel_INFO, pb.LogType_CORE, msg)
|
||||
CoreState = state
|
||||
info := pb.CoreInfoResponse{
|
||||
CoreState: state,
|
||||
MessageType: msgType,
|
||||
Message: message,
|
||||
}
|
||||
coreInfoObserver.Emit(&info)
|
||||
if useFlutterBridge {
|
||||
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState)})
|
||||
bridge.SendStringToPort(statusPropagationPort, string(msg))
|
||||
}
|
||||
return &info
|
||||
}
|
||||
|
||||
func (s *CoreService) CoreInfoListener(req *pb.Empty, stream grpc.ServerStreamingServer[pb.CoreInfoResponse]) error {
|
||||
coreSub, done, err := coreInfoObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer coreInfoObserver.UnSubscribe(coreSub)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-done:
|
||||
return nil
|
||||
case info := <-coreSub:
|
||||
stream.Send(info)
|
||||
// case <-time.After(500 * time.Millisecond):
|
||||
// info := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_EMPTY, "")
|
||||
// stream.Send(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/hiddify/hiddify-core/bridge"
|
||||
"github.com/hiddify/hiddify-core/config"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var (
|
||||
Box *libbox.BoxService
|
||||
HiddifyOptions *config.HiddifyOptions
|
||||
activeConfigPath string
|
||||
coreLogFactory log.Factory
|
||||
useFlutterBridge bool = true
|
||||
)
|
||||
|
||||
func StopAndAlert(msgType pb.MessageType, message string) {
|
||||
SetCoreStatus(pb.CoreState_STOPPED, msgType, message)
|
||||
config.DeactivateTunnelService()
|
||||
if oldCommandServer != nil {
|
||||
oldCommandServer.SetService(nil)
|
||||
}
|
||||
if Box != nil {
|
||||
Box.Close()
|
||||
Box = nil
|
||||
}
|
||||
if oldCommandServer != nil {
|
||||
oldCommandServer.Close()
|
||||
}
|
||||
if useFlutterBridge {
|
||||
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) {
|
||||
return Start(in)
|
||||
}
|
||||
|
||||
func Start(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
|
||||
defer config.DeferPanicToError("start", func(err 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")
|
||||
Stop()
|
||||
// return &pb.CoreInfoResponse{
|
||||
// CoreState: CoreState,
|
||||
// MessageType: pb.MessageType_INSTANCE_NOT_STOPPED,
|
||||
// }, fmt.Errorf("instance not stopped")
|
||||
}
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Core")
|
||||
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
|
||||
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
|
||||
resp, err := StartService(in)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (s *CoreService) StartService(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
|
||||
return StartService(in)
|
||||
}
|
||||
|
||||
func StartService(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Core Service")
|
||||
content := in.ConfigContent
|
||||
if content == "" {
|
||||
|
||||
activeConfigPath = in.ConfigPath
|
||||
fileContent, err := os.ReadFile(activeConfigPath)
|
||||
if err != nil {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
|
||||
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_READING_CONFIG, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
content = string(fileContent)
|
||||
}
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Parsing Config")
|
||||
|
||||
parsedContent, err := readOptions(content)
|
||||
Log(pb.LogLevel_DEBUG, 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())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
if !in.EnableRawConfig {
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Building config")
|
||||
parsedContent_tmp, err := config.BuildConfig(*HiddifyOptions, 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())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
parsedContent = *parsedContent_tmp
|
||||
}
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Saving config")
|
||||
currentBuildConfigPath := filepath.Join(sWorkingPath, "current-config.json")
|
||||
config.SaveCurrentConfig(currentBuildConfigPath, parsedContent)
|
||||
if activeConfigPath == "" {
|
||||
activeConfigPath = currentBuildConfigPath
|
||||
}
|
||||
if in.EnableOldCommandServer {
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Command Server")
|
||||
err = startCommandServer()
|
||||
if err != nil {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
|
||||
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_COMMAND_SERVER, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
Log(pb.LogLevel_DEBUG, 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())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Service.. started")
|
||||
if in.DelayStart {
|
||||
<-time.After(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
err = instance.Start()
|
||||
if err != nil {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
|
||||
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_SERVICE, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
return resp, err
|
||||
}
|
||||
Box = instance
|
||||
if in.EnableOldCommandServer {
|
||||
oldCommandServer.SetService(Box)
|
||||
}
|
||||
|
||||
resp := SetCoreStatus(pb.CoreState_STARTED, pb.MessageType_EMPTY, "")
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) Parse(ctx context.Context, in *pb.ParseRequest) (*pb.ParseResponse, error) {
|
||||
return Parse(in)
|
||||
}
|
||||
|
||||
func Parse(in *pb.ParseRequest) (*pb.ParseResponse, error) {
|
||||
defer config.DeferPanicToError("parse", func(err error) {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CONFIG, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
})
|
||||
|
||||
content := in.Content
|
||||
if in.TempPath != "" {
|
||||
contentBytes, err := os.ReadFile(in.TempPath)
|
||||
content = string(contentBytes)
|
||||
os.Chdir(filepath.Dir(in.ConfigPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
config, err := config.ParseConfigContent(content, true, HiddifyOptions, false)
|
||||
if err != nil {
|
||||
return &pb.ParseResponse{
|
||||
ResponseCode: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
if in.ConfigPath != "" {
|
||||
err = os.WriteFile(in.ConfigPath, config, 0o644)
|
||||
if err != nil {
|
||||
return &pb.ParseResponse{
|
||||
ResponseCode: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
}
|
||||
return &pb.ParseResponse{
|
||||
ResponseCode: pb.ResponseCode_OK,
|
||||
Content: string(config),
|
||||
Message: "",
|
||||
}, err
|
||||
}
|
||||
|
||||
func (s *CoreService) ChangeHiddifySettings(ctx context.Context, in *pb.ChangeHiddifySettingsRequest) (*pb.CoreInfoResponse, error) {
|
||||
return ChangeHiddifySettings(in)
|
||||
}
|
||||
|
||||
func ChangeHiddifySettings(in *pb.ChangeHiddifySettingsRequest) (*pb.CoreInfoResponse, error) {
|
||||
HiddifyOptions = config.DefaultHiddifyOptions()
|
||||
err := json.Unmarshal([]byte(in.HiddifySettingsJson), HiddifyOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if HiddifyOptions.Warp.WireguardConfigStr != "" {
|
||||
err := json.Unmarshal([]byte(HiddifyOptions.Warp.WireguardConfigStr), &HiddifyOptions.Warp.WireguardConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if HiddifyOptions.Warp2.WireguardConfigStr != "" {
|
||||
err := json.Unmarshal([]byte(HiddifyOptions.Warp2.WireguardConfigStr), &HiddifyOptions.Warp2.WireguardConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pb.CoreInfoResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) GenerateConfig(ctx context.Context, in *pb.GenerateConfigRequest) (*pb.GenerateConfigResponse, error) {
|
||||
return GenerateConfig(in)
|
||||
}
|
||||
|
||||
func GenerateConfig(in *pb.GenerateConfigRequest) (*pb.GenerateConfigResponse, error) {
|
||||
defer config.DeferPanicToError("generateConfig", func(err error) {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CONFIG, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
})
|
||||
if HiddifyOptions == nil {
|
||||
HiddifyOptions = config.DefaultHiddifyOptions()
|
||||
}
|
||||
config, err := generateConfigFromFile(in.Path, *HiddifyOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.GenerateConfigResponse{
|
||||
ConfigContent: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateConfigFromFile(path string, configOpt config.HiddifyOptions) (string, error) {
|
||||
os.Chdir(filepath.Dir(path))
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
options, err := readOptions(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config, err := config.BuildConfigJson(configOpt, options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) Stop(ctx context.Context, empty *pb.Empty) (*pb.CoreInfoResponse, error) {
|
||||
return Stop()
|
||||
}
|
||||
|
||||
func Stop() (*pb.CoreInfoResponse, error) {
|
||||
defer config.DeferPanicToError("stop", func(err error) {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
})
|
||||
|
||||
if CoreState != pb.CoreState_STARTED {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, "Core is not started")
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_INSTANCE_NOT_STARTED,
|
||||
Message: "instance is not started",
|
||||
}, fmt.Errorf("instance not started")
|
||||
}
|
||||
if Box == nil {
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_INSTANCE_NOT_FOUND,
|
||||
Message: "instance is not found",
|
||||
}, fmt.Errorf("instance not found")
|
||||
}
|
||||
SetCoreStatus(pb.CoreState_STOPPING, pb.MessageType_EMPTY, "")
|
||||
config.DeactivateTunnelService()
|
||||
if oldCommandServer != nil {
|
||||
oldCommandServer.SetService(nil)
|
||||
}
|
||||
|
||||
err := Box.Close()
|
||||
if err != nil {
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_UNEXPECTED_ERROR,
|
||||
Message: "Error while stopping the service.",
|
||||
}, fmt.Errorf("Error while stopping the service.")
|
||||
}
|
||||
Box = nil
|
||||
if oldCommandServer != nil {
|
||||
err = oldCommandServer.Close()
|
||||
if err != nil {
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_UNEXPECTED_ERROR,
|
||||
Message: "Error while Closing the comand server.",
|
||||
}, fmt.Errorf("error while Closing the comand server.")
|
||||
}
|
||||
oldCommandServer = nil
|
||||
}
|
||||
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_EMPTY, "")
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) Restart(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
|
||||
return Restart(in)
|
||||
}
|
||||
|
||||
func Restart(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
|
||||
defer config.DeferPanicToError("restart", func(err error) {
|
||||
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
|
||||
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
|
||||
})
|
||||
log.Debug("[Service] Restarting")
|
||||
|
||||
if CoreState != pb.CoreState_STARTED {
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_INSTANCE_NOT_STARTED,
|
||||
Message: "instance is not started",
|
||||
}, fmt.Errorf("instance not started")
|
||||
}
|
||||
if Box == nil {
|
||||
return &pb.CoreInfoResponse{
|
||||
CoreState: CoreState,
|
||||
MessageType: pb.MessageType_INSTANCE_NOT_FOUND,
|
||||
Message: "instance is not found",
|
||||
}, fmt.Errorf("instance not found")
|
||||
}
|
||||
|
||||
resp, err := Stop()
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
|
||||
<-time.After(250 * time.Millisecond)
|
||||
|
||||
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
|
||||
resp, gErr := StartService(in)
|
||||
return resp, gErr
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
tmdb "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
// getDB initializes the database with retry logic. If it fails after 100 attempts, it returns nil.
|
||||
func getDB(name string, readOnly bool) (tmdb.DB, error) {
|
||||
// Check if the database file exists; if not, set to readOnly
|
||||
dbPath := "data/" + name + ".db"
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
readOnly = false
|
||||
}
|
||||
|
||||
const retryAttempts = 100
|
||||
const retryDelay = 100 * time.Microsecond
|
||||
|
||||
var db tmdb.DB
|
||||
var err error
|
||||
|
||||
for i := 0; i < retryAttempts; i++ {
|
||||
// Set readOnly to true for the first 80 attempts
|
||||
opts := &opt.Options{ReadOnly: readOnly && i < 80}
|
||||
|
||||
db, err = tmdb.NewGoLevelDBWithOpts(name, "./data", opts)
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
log.Printf("Failed attempt %d to initialize the database: %v", i, err)
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GetTable returns a new Table instance for the generic type T, ensuring the struct has an "Id" field.
|
||||
func GetTable[T any]() *Table[T] {
|
||||
var t T
|
||||
typeName := reflect.TypeOf(t).Name()
|
||||
if !hasIdField(t) {
|
||||
panic(fmt.Sprintf("Table %s must have a field named 'Id'", typeName))
|
||||
}
|
||||
return &Table[T]{name: typeName}
|
||||
}
|
||||
|
||||
// hasIdField checks if the struct has a field named "Id".
|
||||
func hasIdField[T any](t T) bool {
|
||||
val := reflect.Indirect(reflect.ValueOf(t))
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
return val.FieldByName("Id").IsValid()
|
||||
}
|
||||
|
||||
// getIdBytes converts an ID to its byte representation for storage in the database.
|
||||
func getIdBytes(id any) []byte {
|
||||
res, err := SerializeKey(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// if id == nil {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// var buf bytes.Buffer
|
||||
// switch v := id.(type) {
|
||||
// case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
// if err := binary.Write(&buf, binary.BigEndian, v); err == nil {
|
||||
// return buf.Bytes()
|
||||
// }
|
||||
// case string:
|
||||
// return []byte(v)
|
||||
// case []byte:
|
||||
// return v
|
||||
// default:
|
||||
// return []byte(fmt.Sprint(v))
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// getId retrieves the "Id" field from the struct T.
|
||||
func getId[T any](t T) any {
|
||||
val := reflect.Indirect(reflect.ValueOf(t))
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
field := val.FieldByName("Id")
|
||||
if field.IsValid() {
|
||||
return field.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Table represents a database table for generic type T.
|
||||
type Table[T any] struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// All retrieves all entries from the database and unmarshals them into a slice of T.
|
||||
func (tbl *Table[T]) All() ([]*T, error) {
|
||||
db, err := getDB(tbl.name, true)
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var items []*T
|
||||
iter, err := db.Iterator(nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
|
||||
item, err := Deserialize[T](iter.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func Serialize(data any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(data)
|
||||
return buf.Bytes(), err
|
||||
|
||||
// return json.Marshal(data)
|
||||
}
|
||||
|
||||
func SerializeKey(data any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(data)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func Deserialize[T any](data []byte) (*T, error) {
|
||||
var obj T
|
||||
buf := bytes.NewBuffer(data)
|
||||
dec := gob.NewDecoder(buf)
|
||||
err := dec.Decode(&obj)
|
||||
return &obj, err
|
||||
|
||||
// return &obj, json.Unmarshal(data, &obj)
|
||||
}
|
||||
|
||||
// UpdateInsert inserts or updates multiple items in the database.
|
||||
func (tbl *Table[T]) UpdateInsert(items ...*T) error {
|
||||
db, err := getDB(tbl.name, false)
|
||||
if db == nil {
|
||||
return fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
for _, item := range items {
|
||||
// b, err := json.Marshal(item)
|
||||
b, err := Serialize(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Set(getIdBytes(getId(item)), b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes entries by their IDs.
|
||||
func (tbl *Table[T]) Delete(ids ...any) error {
|
||||
db, err := getDB(tbl.name, true)
|
||||
if db == nil {
|
||||
return fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
for _, id := range ids {
|
||||
if err := db.Delete(getIdBytes(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a single item by its ID.
|
||||
func (tbl *Table[T]) Get(id any) (*T, error) {
|
||||
db, err := getDB(tbl.name, true)
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
b, err := db.Get(getIdBytes(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Deserialize[T](b)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
address = "localhost:50051"
|
||||
defaultName = "world"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.Dial(address, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
log.Fatalf("did not connect: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
c := pb.NewHelloClient(conn)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// SayHello
|
||||
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: defaultName})
|
||||
if err != nil {
|
||||
log.Fatalf("could not greet: %v", err)
|
||||
}
|
||||
log.Printf("Greeting: %s", r.Message)
|
||||
|
||||
// SayHelloStream
|
||||
stream, err := c.SayHelloStream(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("could not stream: %v", err)
|
||||
}
|
||||
|
||||
names := []string{"Alice", "Bob", "Charlie"}
|
||||
|
||||
for _, name := range names {
|
||||
err := stream.Send(&pb.HelloRequest{Name: name})
|
||||
if err != nil {
|
||||
log.Fatalf("could not send: %v", err)
|
||||
}
|
||||
r, err := stream.Recv()
|
||||
if err != nil {
|
||||
log.Fatalf("could not receive: %v", err)
|
||||
}
|
||||
log.Printf("Received1: %s", r.Message)
|
||||
r2, err2 := stream.Recv()
|
||||
if err2 != nil {
|
||||
log.Fatalf("could not receive2: %v", err2)
|
||||
}
|
||||
log.Printf("Received: %s", r2.Message)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
v2 "github.com/hiddify/hiddify-core/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// defer C.free(unsafe.Pointer(port))
|
||||
v2.StartGrpcServer("127.0.0.1:50051", "hello")
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package v2
|
||||
|
||||
/*
|
||||
#include "stdint.h"
|
||||
*/
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/hiddify/hiddify-core/extension"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type HelloService struct {
|
||||
pb.UnimplementedHelloServer
|
||||
}
|
||||
type CoreService struct {
|
||||
pb.UnimplementedCoreServer
|
||||
}
|
||||
|
||||
type TunnelService struct {
|
||||
pb.UnimplementedTunnelServiceServer
|
||||
}
|
||||
|
||||
func StartGrpcServer(listenAddressG string, service string) (*grpc.Server, error) {
|
||||
lis, err := net.Listen("tcp", listenAddressG)
|
||||
if err != nil {
|
||||
log.Printf("failed to listen: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
if service == "core" {
|
||||
|
||||
// Setup("./tmp/", "./tmp", "./tmp", 11111, false)
|
||||
|
||||
useFlutterBridge = false
|
||||
pb.RegisterCoreServer(s, &CoreService{})
|
||||
pb.RegisterExtensionHostServiceServer(s, &extension.ExtensionHostService{})
|
||||
} else if service == "hello" {
|
||||
pb.RegisterHelloServer(s, &HelloService{})
|
||||
} else if service == "tunnel" {
|
||||
pb.RegisterTunnelServiceServer(s, &TunnelService{})
|
||||
}
|
||||
log.Printf("Server listening on %s", listenAddressG)
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Printf("failed to serve: %v", err)
|
||||
}
|
||||
log.Printf("Server stopped")
|
||||
// cancel()
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func StartCoreGrpcServer(listenAddressG string) (*grpc.Server, error) {
|
||||
return StartGrpcServer(listenAddressG, "core")
|
||||
}
|
||||
|
||||
func StartHelloGrpcServer(listenAddressG string) (*grpc.Server, error) {
|
||||
return StartGrpcServer(listenAddressG, "hello")
|
||||
}
|
||||
|
||||
func StartTunnelGrpcServer(listenAddressG string) (*grpc.Server, error) {
|
||||
return StartGrpcServer(listenAddressG, "tunnel")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
)
|
||||
|
||||
func (s *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
|
||||
return &pb.HelloResponse{Message: "Hello, " + in.Name}, nil
|
||||
}
|
||||
func (s *HelloService) SayHelloStream(stream pb.Hello_SayHelloStreamServer) error {
|
||||
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
log.Printf("stream.Recv() failed: %v", err)
|
||||
break
|
||||
}
|
||||
log.Printf("Received: %v", req.Name)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = stream.Send(&pb.HelloResponse{Message: "Hello, " + req.Name})
|
||||
if err != nil {
|
||||
log.Printf("stream.Send() failed: %v", err)
|
||||
break
|
||||
}
|
||||
err = stream.Send(&pb.HelloResponse{Message: "Hello again, " + req.Name})
|
||||
if err != nil {
|
||||
log.Printf("stream.Send() failed: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hiddify/hiddify-core/config"
|
||||
"golang.org/x/net/proxy"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func getRandomAvailblePort() uint16 {
|
||||
// TODO: implement it
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
return uint16(listener.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
func RunInstanceString(hiddifySettings *config.HiddifyOptions, proxiesInput string) (*HiddifyService, error) {
|
||||
if hiddifySettings == nil {
|
||||
hiddifySettings = config.DefaultHiddifyOptions()
|
||||
}
|
||||
singconfigs, err := config.ParseConfigContentToOptions(proxiesInput, true, hiddifySettings, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return RunInstance(hiddifySettings, singconfigs)
|
||||
}
|
||||
|
||||
func RunInstance(hiddifySettings *config.HiddifyOptions, singconfig *option.Options) (*HiddifyService, error) {
|
||||
if hiddifySettings == nil {
|
||||
hiddifySettings = config.DefaultHiddifyOptions()
|
||||
}
|
||||
hiddifySettings.EnableClashApi = false
|
||||
hiddifySettings.InboundOptions.MixedPort = getRandomAvailblePort()
|
||||
hiddifySettings.InboundOptions.EnableTun = false
|
||||
hiddifySettings.InboundOptions.EnableTunService = false
|
||||
hiddifySettings.InboundOptions.SetSystemProxy = false
|
||||
hiddifySettings.InboundOptions.TProxyPort = 0
|
||||
hiddifySettings.InboundOptions.LocalDnsPort = 0
|
||||
hiddifySettings.Region = "other"
|
||||
hiddifySettings.BlockAds = false
|
||||
hiddifySettings.LogFile = "/dev/null"
|
||||
|
||||
finalConfigs, err := config.BuildConfig(*hiddifySettings, *singconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instance, err := NewService(*finalConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = instance.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
<-time.After(250 * time.Millisecond)
|
||||
hservice := &HiddifyService{libbox: instance, ListenPort: hiddifySettings.InboundOptions.MixedPort}
|
||||
hservice.PingCloudflare()
|
||||
return hservice, nil
|
||||
}
|
||||
|
||||
type HiddifyService struct {
|
||||
libbox *libbox.BoxService
|
||||
ListenPort uint16
|
||||
}
|
||||
|
||||
// dialer, err := s.libbox.GetInstance().Router().Dialer(context.Background())
|
||||
|
||||
func (s *HiddifyService) Close() error {
|
||||
return s.libbox.Close()
|
||||
}
|
||||
|
||||
func (s *HiddifyService) GetContent(url string) (string, error) {
|
||||
return s.ContentFromURL("GET", url, 10*time.Second)
|
||||
}
|
||||
|
||||
func (s *HiddifyService) ContentFromURL(method string, url string, timeout time.Duration) (string, error) {
|
||||
if method == "" {
|
||||
return "", fmt.Errorf("empty method")
|
||||
}
|
||||
if url == "" {
|
||||
return "", fmt.Errorf("empty url")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("127.0.0.1:%d", s.ListenPort), nil, proxy.Direct)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
return "", fmt.Errorf("empty body")
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *HiddifyService) PingCloudflare() (time.Duration, error) {
|
||||
return s.Ping("http://cp.cloudflare.com")
|
||||
}
|
||||
|
||||
// func (s *HiddifyService) RawConnection(ctx context.Context, url string) (net.Conn, error) {
|
||||
// return
|
||||
// }
|
||||
|
||||
func (s *HiddifyService) PingAverage(url string, count int) (time.Duration, error) {
|
||||
if count <= 0 {
|
||||
return -1, fmt.Errorf("count must be greater than 0")
|
||||
}
|
||||
|
||||
var sum int
|
||||
real_count := 0
|
||||
for i := 0; i < count; i++ {
|
||||
delay, err := s.Ping(url)
|
||||
if err == nil {
|
||||
real_count++
|
||||
sum += int(delay.Milliseconds())
|
||||
} else if real_count == 0 && i > count/2 {
|
||||
return -1, fmt.Errorf("ping average failed")
|
||||
}
|
||||
|
||||
}
|
||||
return time.Duration(sum / real_count * int(time.Millisecond)), nil
|
||||
}
|
||||
|
||||
func (s *HiddifyService) Ping(url string) (time.Duration, error) {
|
||||
startTime := time.Now()
|
||||
_, err := s.ContentFromURL("HEAD", url, 4*time.Second)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
duration := time.Since(startTime)
|
||||
return duration, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func NewObserver[T any](listenerBufferSize int) *observable.Observer[T] {
|
||||
return observable.NewObserver(observable.NewSubscriber[T](listenerBufferSize), listenerBufferSize)
|
||||
}
|
||||
|
||||
var logObserver = NewObserver[pb.LogMessage](10)
|
||||
|
||||
func Log(level pb.LogLevel, typ pb.LogType, message string) {
|
||||
if level != pb.LogLevel_DEBUG {
|
||||
fmt.Printf("%s %s %s\n", level, typ, message)
|
||||
}
|
||||
logObserver.Emit(pb.LogMessage{
|
||||
Level: level,
|
||||
Type: typ,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CoreService) LogListener(req *pb.Empty, stream grpc.ServerStreamingServer[pb.LogMessage]) error {
|
||||
logSub, stopch, _ := logObserver.Subscribe()
|
||||
defer logObserver.UnSubscribe(logSub)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-stopch:
|
||||
return nil
|
||||
case info := <-logSub:
|
||||
stream.Send(&info)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hiddify/hiddify-core/bridge"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var (
|
||||
_ libbox.CommandClientHandler = (*OldCommandClientHandler)(nil)
|
||||
)
|
||||
|
||||
type OldCommandClientHandler struct {
|
||||
port int64
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) Connected() {
|
||||
cch.logger.Debug("CONNECTED")
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) Disconnected(message string) {
|
||||
cch.logger.Debug("DISCONNECTED: ", message)
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) ClearLog() {
|
||||
cch.logger.Debug("clear log")
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) WriteLog(message string) {
|
||||
cch.logger.Debug("log: ", message)
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) WriteStatus(message *libbox.StatusMessage) {
|
||||
msg, err := json.Marshal(
|
||||
map[string]int64{
|
||||
"connections-in": int64(message.ConnectionsIn),
|
||||
"connections-out": int64(message.ConnectionsOut),
|
||||
"uplink": message.Uplink,
|
||||
"downlink": message.Downlink,
|
||||
"uplink-total": message.UplinkTotal,
|
||||
"downlink-total": message.DownlinkTotal,
|
||||
},
|
||||
)
|
||||
cch.logger.Debug("Memory: ", libbox.FormatBytes(message.Memory), ", Goroutines: ", message.Goroutines)
|
||||
if err != nil {
|
||||
bridge.SendStringToPort(cch.port, fmt.Sprintf("error: %e", err))
|
||||
} else {
|
||||
bridge.SendStringToPort(cch.port, string(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) WriteGroups(message libbox.OutboundGroupIterator) {
|
||||
if message == nil {
|
||||
return
|
||||
}
|
||||
groups := []*OutboundGroup{}
|
||||
for message.HasNext() {
|
||||
group := message.Next()
|
||||
items := group.GetItems()
|
||||
groupItems := []*OutboundGroupItem{}
|
||||
for items.HasNext() {
|
||||
item := items.Next()
|
||||
groupItems = append(groupItems,
|
||||
&OutboundGroupItem{
|
||||
Tag: item.Tag,
|
||||
Type: item.Type,
|
||||
URLTestTime: item.URLTestTime,
|
||||
URLTestDelay: item.URLTestDelay,
|
||||
},
|
||||
)
|
||||
}
|
||||
groups = append(groups, &OutboundGroup{Tag: group.Tag, Type: group.Type, Selected: group.Selected, Items: groupItems})
|
||||
}
|
||||
response, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
bridge.SendStringToPort(cch.port, fmt.Sprintf("error: %e", err))
|
||||
} else {
|
||||
bridge.SendStringToPort(cch.port, string(response))
|
||||
}
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) InitializeClashMode(modeList libbox.StringIterator, currentMode string) {
|
||||
cch.logger.Debug("initial clash mode: ", currentMode)
|
||||
}
|
||||
|
||||
func (cch *OldCommandClientHandler) UpdateClashMode(newMode string) {
|
||||
cch.logger.Debug("update clash mode: ", newMode)
|
||||
}
|
||||
|
||||
type OutboundGroup struct {
|
||||
Tag string `json:"tag"`
|
||||
Type string `json:"type"`
|
||||
Selected string `json:"selected"`
|
||||
Items []*OutboundGroupItem `json:"items"`
|
||||
}
|
||||
|
||||
type OutboundGroupItem struct {
|
||||
Tag string `json:"tag"`
|
||||
Type string `json:"type"`
|
||||
URLTestTime int64 `json:"url-test-time"`
|
||||
URLTestDelay int32 `json:"url-test-delay"`
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var oldCommandServer *libbox.CommandServer
|
||||
|
||||
type CommandServerHandler struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (csh *CommandServerHandler) ServiceReload() error {
|
||||
csh.logger.Trace("Reloading service")
|
||||
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
|
||||
|
||||
if oldCommandServer != nil {
|
||||
oldCommandServer.SetService(nil)
|
||||
oldCommandServer = nil
|
||||
}
|
||||
if Box != nil {
|
||||
Box.Close()
|
||||
Box = nil
|
||||
}
|
||||
_, err := StartService(&pb.StartRequest{
|
||||
EnableOldCommandServer: true,
|
||||
DelayStart: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (csh *CommandServerHandler) GetSystemProxyStatus() *libbox.SystemProxyStatus {
|
||||
csh.logger.Trace("Getting system proxy status")
|
||||
return &libbox.SystemProxyStatus{Available: true, Enabled: false}
|
||||
}
|
||||
|
||||
func (csh *CommandServerHandler) SetSystemProxyEnabled(isEnabled bool) error {
|
||||
csh.logger.Trace("Setting system proxy status, enabled? ", isEnabled)
|
||||
return csh.ServiceReload()
|
||||
}
|
||||
|
||||
func (csh *CommandServerHandler) PostServiceClose() {
|
||||
|
||||
}
|
||||
func startCommandServer() error {
|
||||
logger := coreLogFactory.NewLogger("[Command Server Handler]")
|
||||
logger.Trace("Starting command server")
|
||||
oldCommandServer = libbox.NewCommandServer(&CommandServerHandler{logger: logger}, 300)
|
||||
return oldCommandServer.Start()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
)
|
||||
|
||||
var (
|
||||
oldStatusClient *libbox.CommandClient
|
||||
oldGroupClient *libbox.CommandClient
|
||||
oldGroupInfoOnlyClient *libbox.CommandClient
|
||||
)
|
||||
|
||||
func StartCommand(command int32, port int64) error {
|
||||
switch command {
|
||||
case libbox.CommandStatus:
|
||||
oldStatusClient = libbox.NewCommandClient(
|
||||
&OldCommandClientHandler{
|
||||
port: port,
|
||||
logger: coreLogFactory.NewLogger("[Status Command Client]"),
|
||||
},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandStatus,
|
||||
StatusInterval: 1000000000, //1000ms debounce
|
||||
},
|
||||
)
|
||||
return oldStatusClient.Connect()
|
||||
case libbox.CommandGroup:
|
||||
oldGroupClient = libbox.NewCommandClient(
|
||||
&OldCommandClientHandler{
|
||||
port: port,
|
||||
logger: coreLogFactory.NewLogger("[Group Command Client]"),
|
||||
},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandGroup,
|
||||
StatusInterval: 300000000, //300ms debounce
|
||||
},
|
||||
)
|
||||
return oldGroupClient.Connect()
|
||||
case libbox.CommandGroupInfoOnly:
|
||||
oldGroupInfoOnlyClient = libbox.NewCommandClient(
|
||||
&OldCommandClientHandler{
|
||||
port: port,
|
||||
logger: coreLogFactory.NewLogger("[GroupInfoOnly Command Client]"),
|
||||
},
|
||||
&libbox.CommandClientOptions{
|
||||
Command: libbox.CommandGroupInfoOnly,
|
||||
StatusInterval: 300000000, //300ms debounce
|
||||
},
|
||||
)
|
||||
return oldGroupInfoOnlyClient.Connect()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopCommand(command int32) error {
|
||||
switch command {
|
||||
case libbox.CommandStatus:
|
||||
err := oldStatusClient.Disconnect()
|
||||
oldStatusClient = nil
|
||||
return err
|
||||
case libbox.CommandGroup:
|
||||
err := oldGroupClient.Disconnect()
|
||||
oldGroupClient = nil
|
||||
return err
|
||||
case libbox.CommandGroupInfoOnly:
|
||||
err := oldGroupInfoOnlyClient.Disconnect()
|
||||
oldGroupInfoOnlyClient = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package v2
|
||||
|
||||
import pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
|
||||
const (
|
||||
Stopped = "Stopped"
|
||||
Starting = "Starting"
|
||||
Started = "Started"
|
||||
Stopping = "Stopping"
|
||||
)
|
||||
|
||||
const (
|
||||
EmptyConfiguration = "EmptyConfiguration"
|
||||
StartCommandServer = "StartCommandServer"
|
||||
CreateService = "CreateService"
|
||||
)
|
||||
|
||||
func convert2OldState(newStatus pb.CoreState) string {
|
||||
if newStatus == pb.CoreState_STOPPED {
|
||||
return Stopped
|
||||
}
|
||||
if newStatus == pb.CoreState_STARTED {
|
||||
return Started
|
||||
}
|
||||
if newStatus == pb.CoreState_STARTING {
|
||||
return Starting
|
||||
}
|
||||
if newStatus == pb.CoreState_STOPPING {
|
||||
return Stopping
|
||||
}
|
||||
return "Invalid"
|
||||
}
|
||||
|
||||
type StatusMessage struct {
|
||||
Status string `json:"status"`
|
||||
Alert *string `json:"alert"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
runtimeDebug "runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/hiddify/hiddify-core/v2/service_manager"
|
||||
|
||||
B "github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var (
|
||||
sWorkingPath string
|
||||
sTempPath string
|
||||
sUserID int
|
||||
sGroupID int
|
||||
statusPropagationPort int64
|
||||
)
|
||||
|
||||
func InitHiddifyService() error {
|
||||
return service_manager.StartServices()
|
||||
}
|
||||
|
||||
func Setup(basePath string, workingPath string, tempPath string, statusPort int64, debug bool) error {
|
||||
statusPropagationPort = int64(statusPort)
|
||||
tcpConn := runtime.GOOS == "windows" // TODO add TVOS
|
||||
libbox.Setup(basePath, workingPath, tempPath, tcpConn)
|
||||
sWorkingPath = workingPath
|
||||
os.Chdir(sWorkingPath)
|
||||
sTempPath = tempPath
|
||||
sUserID = os.Getuid()
|
||||
sGroupID = os.Getgid()
|
||||
|
||||
var defaultWriter io.Writer
|
||||
if !debug {
|
||||
defaultWriter = io.Discard
|
||||
}
|
||||
factory, err := log.New(
|
||||
log.Options{
|
||||
DefaultWriter: defaultWriter,
|
||||
BaseTime: time.Now(),
|
||||
Observable: true,
|
||||
// Options: option.LogOptions{
|
||||
// Disabled: false,
|
||||
// Level: "trace",
|
||||
// Output: "stdout",
|
||||
// },
|
||||
})
|
||||
coreLogFactory = factory
|
||||
|
||||
if err != nil {
|
||||
return E.Cause(err, "create logger")
|
||||
}
|
||||
return InitHiddifyService()
|
||||
}
|
||||
|
||||
func NewService(options option.Options) (*libbox.BoxService, error) {
|
||||
runtimeDebug.FreeOSMemory()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
|
||||
urlTestHistoryStorage := urltest.NewHistoryStorage()
|
||||
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
|
||||
instance, err := B.New(B.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
runtimeDebug.FreeOSMemory()
|
||||
service := libbox.NewBoxService(
|
||||
ctx,
|
||||
cancel,
|
||||
instance,
|
||||
service.FromContext[pause.Manager](ctx),
|
||||
urlTestHistoryStorage,
|
||||
)
|
||||
return &service, nil
|
||||
}
|
||||
|
||||
func readOptions(configContent string) (option.Options, error) {
|
||||
var options option.Options
|
||||
err := options.UnmarshalJSON([]byte(configContent))
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "decode config")
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package service_manager
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
var (
|
||||
services = []adapter.Service{}
|
||||
preservices = []adapter.Service{}
|
||||
)
|
||||
|
||||
func RegisterPreservice(service adapter.Service) {
|
||||
preservices = append(services, service)
|
||||
}
|
||||
|
||||
func Register(service adapter.Service) {
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
func StartServices() error {
|
||||
CloseServices()
|
||||
for _, service := range preservices {
|
||||
if err := service.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, service := range services {
|
||||
if err := service.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CloseServices() error {
|
||||
for _, service := range services {
|
||||
if err := service.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, service := range preservices {
|
||||
if err := service.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hiddify/hiddify-core/config"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func RunStandalone(hiddifySettingPath string, configPath string, defaultConfig config.HiddifyOptions) error {
|
||||
fmt.Println("Running in standalone mode")
|
||||
useFlutterBridge = false
|
||||
current, err := readAndBuildConfig(hiddifySettingPath, configPath, &defaultConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Error in read and build config %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
go StartService(&pb.StartRequest{
|
||||
ConfigContent: current.Config,
|
||||
EnableOldCommandServer: false,
|
||||
DelayStart: false,
|
||||
EnableRawConfig: true,
|
||||
})
|
||||
go updateConfigInterval(current, hiddifySettingPath, configPath)
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
fmt.Printf("Waiting for CTRL+C to stop\n")
|
||||
<-sigChan
|
||||
fmt.Printf("CTRL+C recived-->stopping\n")
|
||||
_, err = Stop()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type ConfigResult struct {
|
||||
Config string
|
||||
RefreshInterval int
|
||||
HiddifyHiddifyOptions *config.HiddifyOptions
|
||||
}
|
||||
|
||||
func readAndBuildConfig(hiddifySettingPath string, configPath string, defaultConfig *config.HiddifyOptions) (ConfigResult, error) {
|
||||
var result ConfigResult
|
||||
|
||||
result, err := readConfigContent(configPath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
hiddifyconfig := config.DefaultHiddifyOptions()
|
||||
|
||||
if defaultConfig != nil {
|
||||
hiddifyconfig = defaultConfig
|
||||
}
|
||||
|
||||
if hiddifySettingPath != "" {
|
||||
hiddifyconfig, err = ReadHiddifyOptionsAt(hiddifySettingPath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
result.HiddifyHiddifyOptions = hiddifyconfig
|
||||
result.Config, err = buildConfig(result.Config, *result.HiddifyHiddifyOptions)
|
||||
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/2.3.1 ("+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.HiddifyOptions) (string, error) {
|
||||
parsedContent, err := config.ParseConfigContent(configContent, true, &options, false)
|
||||
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"
|
||||
}
|
||||
|
||||
fmt.Printf("Open http://localhost:6756/ui/?secret=%s in your browser\n", finalconfig.Experimental.ClashAPI.Secret)
|
||||
|
||||
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, current.HiddifyHiddifyOptions)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if new.Config != current.Config {
|
||||
go Stop()
|
||||
go StartService(&pb.StartRequest{
|
||||
ConfigContent: new.Config,
|
||||
DelayStart: false,
|
||||
EnableOldCommandServer: false,
|
||||
DisableMemoryLimit: false,
|
||||
EnableRawConfig: true,
|
||||
})
|
||||
}
|
||||
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 ReadHiddifyOptionsAt(path string) (*config.HiddifyOptions, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var options config.HiddifyOptions
|
||||
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
|
||||
}
|
||||
}
|
||||
if options.Warp2.WireguardConfigStr != "" {
|
||||
err := json.Unmarshal([]byte(options.Warp2.WireguardConfigStr), &options.Warp2.WireguardConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &options, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
)
|
||||
|
||||
func (s *CoreService) GetSystemProxyStatus(ctx context.Context, empty *pb.Empty) (*pb.SystemProxyStatus, error) {
|
||||
return GetSystemProxyStatus(ctx, empty)
|
||||
}
|
||||
func GetSystemProxyStatus(ctx context.Context, empty *pb.Empty) (*pb.SystemProxyStatus, error) {
|
||||
status, err := libbox.NewStandaloneCommandClient().GetSystemProxyStatus()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.SystemProxyStatus{
|
||||
Available: status.Available,
|
||||
Enabled: status.Enabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CoreService) SetSystemProxyEnabled(ctx context.Context, in *pb.SetSystemProxyEnabledRequest) (*pb.Response, error) {
|
||||
return SetSystemProxyEnabled(ctx, in)
|
||||
}
|
||||
func SetSystemProxyEnabled(ctx context.Context, in *pb.SetSystemProxyEnabledRequest) (*pb.Response, error) {
|
||||
err := libbox.NewStandaloneCommandClient().SetSystemProxyEnabled(in.IsEnabled)
|
||||
|
||||
if err != nil {
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return &pb.Response{
|
||||
ResponseCode: pb.ResponseCode_OK,
|
||||
Message: "",
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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 {
|
||||
_, err := StartTunnelGrpcServer(fmt.Sprintf("127.0.0.1:%d", port))
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
)
|
||||
|
||||
func (s *TunnelService) Start(ctx context.Context, in *pb.TunnelStartRequest) (*pb.TunnelResponse, error) {
|
||||
if in.ServerPort == 0 {
|
||||
in.ServerPort = 12334
|
||||
}
|
||||
useFlutterBridge = false
|
||||
res, err := Start(&pb.StartRequest{
|
||||
ConfigContent: makeTunnelConfig(in.Ipv6, in.ServerPort, in.StrictRoute, in.EndpointIndependentNat, in.Stack),
|
||||
EnableOldCommandServer: false,
|
||||
DisableMemoryLimit: true,
|
||||
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 := `{
|
||||
"log":{
|
||||
"level": "warn"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"tag": "tun-in",
|
||||
"interface_name": "HiddifyTunnel",
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
` + ipv6 + `
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"process_name":[
|
||||
"Hiddify.exe",
|
||||
"Hiddify",
|
||||
"HiddifyCli",
|
||||
"HiddifyCli.exe"
|
||||
],
|
||||
"outbound": "direct-out"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
func (s *TunnelService) Stop(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
|
||||
res, err := Stop()
|
||||
log.Printf("Stop Result: %+v\n", res)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hiddify/hiddify-core/config"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
)
|
||||
|
||||
func (s *CoreService) GenerateWarpConfig(ctx context.Context, in *pb.GenerateWarpConfigRequest) (*pb.WarpGenerationResponse, error) {
|
||||
return GenerateWarpConfig(in)
|
||||
}
|
||||
func GenerateWarpConfig(in *pb.GenerateWarpConfigRequest) (*pb.WarpGenerationResponse, error) {
|
||||
identity, log, wg, err := config.GenerateWarpInfo(in.LicenseKey, in.AccountId, in.AccessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.WarpGenerationResponse{
|
||||
Account: &pb.WarpAccount{
|
||||
AccountId: identity.ID,
|
||||
AccessToken: identity.Token,
|
||||
},
|
||||
Config: &pb.WarpWireguardConfig{
|
||||
PrivateKey: wg.PrivateKey,
|
||||
LocalAddressIpv4: wg.LocalAddressIPv4,
|
||||
LocalAddressIpv6: wg.LocalAddressIPv6,
|
||||
PeerPublicKey: wg.PeerPublicKey,
|
||||
ClientId: wg.ClientID,
|
||||
},
|
||||
Log: log,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user