new: add hiddifyrpc

This commit is contained in:
Hiddify
2024-03-03 04:15:19 +01:00
parent 0aa2c99599
commit a4b067006a
22 changed files with 3715 additions and 29 deletions

77
v2/command_client.go Normal file
View File

@@ -0,0 +1,77 @@
package v2
import (
pb "github.com/hiddify/libcore/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)
}

163
v2/commands.go Normal file
View File

@@ -0,0 +1,163 @@
package v2
import (
"context"
"time"
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing/common/observable"
)
var systemInfoObserver = observable.Observer[pb.SystemInfo]{}
var outboundsInfoObserver = observable.Observer[pb.OutboundGroupList]{}
var mainOutboundsInfoObserver = observable.Observer[pb.OutboundGroupList]{}
var (
statusClient *libbox.CommandClient
groupClient *libbox.CommandClient
groupInfoOnlyClient *libbox.CommandClient
)
func (s *server) GetSystemInfo(stream pb.Hiddify_GetSystemInfoServer) 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, _, _ := systemInfoObserver.Subscribe()
stopch := make(chan int)
go func() {
stream.Recv()
close(stopch)
}()
for {
select {
case <-stream.Context().Done():
break
case <-stopch:
break
case info := <-sub:
stream.Send(&info)
case <-time.After(1000 * time.Millisecond):
}
}
}
func (s *server) OutboundsInfo(stream pb.Hiddify_OutboundsInfoServer) 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, _, _ := outboundsInfoObserver.Subscribe()
stopch := make(chan int)
go func() {
stream.Recv()
close(stopch)
}()
for {
select {
case <-stream.Context().Done():
break
case <-stopch:
break
case info := <-sub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}
func (s *server) MainOutboundsInfo(stream pb.Hiddify_MainOutboundsInfoServer) 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, _, _ := mainOutboundsInfoObserver.Subscribe()
stopch := make(chan int)
go func() {
stream.Recv()
close(stopch)
}()
for {
select {
case <-stream.Context().Done():
break
case <-stopch:
break
case info := <-sub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}
// Implement the SelectOutbound method
func (s *server) SelectOutbound(ctx context.Context, 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
}
// Implement the UrlTest method
func (s *server) UrlTest(ctx context.Context, 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
}

44
v2/coreinfo.go Normal file
View File

@@ -0,0 +1,44 @@
package v2
import (
"time"
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing/common/observable"
)
var coreInfoObserver = observable.Observer[pb.CoreInfoResponse]{}
var CoreState = pb.CoreState_STOPPED
func SetCoreStatus(state pb.CoreState, msgType pb.MessageType, message string) pb.CoreInfoResponse {
CoreState = state
info := pb.CoreInfoResponse{
CoreState: state,
MessageType: msgType,
Message: message,
}
coreInfoObserver.Emit(info)
return info
}
func (s *server) CoreInfoListener(stream pb.Hiddify_CoreInfoListenerServer) error {
coreSub, _, _ := coreInfoObserver.Subscribe()
defer coreInfoObserver.UnSubscribe(coreSub)
stopch := make(chan int)
go func() {
stream.Recv()
close(stopch)
}()
for {
select {
case <-stream.Context().Done():
break
case <-stopch:
break
case info := <-coreSub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}

232
v2/custom.go Normal file
View File

@@ -0,0 +1,232 @@
package v2
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/hiddify/libcore/config"
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
var configOptions *config.ConfigOptions
var activeConfigPath *string
var logFactory *log.Factory
func stopAndAlert(msgType pb.MessageType, message string) {
SetCoreStatus(pb.CoreState_STOPPED, msgType, message)
config.DeactivateTunnelService()
// if commandServer != nil {
// commandServer.SetService(nil)
// }
if Box != nil {
Box.Close()
Box = nil
}
// if commandServer != nil {
// commandServer.Close()
// }
}
func (s *server) Start(ctx context.Context, 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())
})
if CoreState != pb.CoreState_STOPPED {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_STOPPED,
}, fmt.Errorf("instance not stopped")
}
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
resp, err := s.StartService(ctx, in)
return resp, err
}
// Implement the StartService method
func (s *server) StartService(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
content := in.ConfigContent
if content != "" {
fileContent, err := os.ReadFile(*activeConfigPath)
if err != nil {
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_READING_CONFIG, err.Error())
return &resp, err
}
content = string(fileContent)
}
parsedContent, err := parseConfig(content)
if err != nil {
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 {
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_BUILDING_CONFIG, err.Error())
return &resp, err
}
config.SaveCurrentConfig(filepath.Join(sWorkingPath, "current-config.json"), *patchedOptions)
// err = startCommandServer(*logFactory)
// if err != nil {
// resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_COMMAND_SERVER, err.Error())
// return &resp, err
// }
instance, err := NewService(*patchedOptions)
if err != nil {
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_CREATE_SERVICE, err.Error())
return &resp, err
}
if in.DelayStart {
<-time.After(250 * time.Millisecond)
}
err = instance.Start()
if err != nil {
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_SERVICE, err.Error())
return &resp, err
}
Box = instance
// commandServer.SetService(box)
resp := SetCoreStatus(pb.CoreState_STARTED, pb.MessageType_EMPTY, "")
return &resp, nil
}
func (s *server) Parse(ctx context.Context, 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())
})
config, err := config.ParseConfigContent(in.Content, true)
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 *server) ChangeConfigOptions(ctx context.Context, in *pb.ChangeConfigOptionsRequest) (*pb.CoreInfoResponse, error) {
// // Implement your change config options logic
// // Return a CoreInfoResponse
// }
// func (s *server) GenerateConfig(ctx context.Context, 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())
// })
// config, err := generateConfigFromFile(C.GoString(path), *configOptions)
// if err != nil {
// return C.CString("error" + err.Error())
// }
// return C.CString(config)
// }
// Implement the Stop method
func (s *server) Stop(ctx context.Context, empty *pb.Empty) (*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())
})
config.DeactivateTunnelService()
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, "")
// commandServer.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
// err = commandServer.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.")
// }
// commandServer = nil
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_EMPTY, "")
return &resp, nil
}
func (s *server) Restart(ctx context.Context, 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 := s.Stop(ctx, &pb.Empty{})
if err != nil {
return resp, err
}
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
<-time.After(250 * time.Millisecond)
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
resp, gErr := s.StartService(ctx, in)
return resp, gErr
}

62
v2/example_client/main.go Normal file
View File

@@ -0,0 +1,62 @@
package main
import (
"context"
"log"
"time"
pb "github.com/hiddify/libcore/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.NewHiddifyClient(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)
}
}

16
v2/example_server/main.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"os"
"os/signal"
"syscall"
v2 "github.com/hiddify/libcore/v2"
)
func main() {
v2.StartGrpcServer()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
}

34
v2/grpc_server.go Normal file
View File

@@ -0,0 +1,34 @@
package v2
import "C"
import (
"log"
"net"
pb "github.com/hiddify/libcore/hiddifyrpc"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
type server struct {
pb.UnimplementedHiddifyServer
}
//export StartGrpcServer
func StartGrpcServer() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Printf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterHiddifyServer(s, &server{})
log.Printf("Server listening on %s", port)
go func() {
if err := s.Serve(lis); err != nil {
log.Printf("failed to serve: %v", err)
}
}()
}

36
v2/hello.go Normal file
View File

@@ -0,0 +1,36 @@
package v2
import (
"context"
"log"
"time"
pb "github.com/hiddify/libcore/hiddifyrpc"
)
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello, " + in.Name}, nil
}
func (s *server) SayHelloStream(stream pb.Hiddify_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
}

41
v2/logproto.go Normal file
View File

@@ -0,0 +1,41 @@
package v2
import (
"time"
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing/common/observable"
)
var logObserver = observable.Observer[pb.LogMessage]{}
func Log(level pb.LogLevel, typ pb.LogType, message string) {
logObserver.Emit(pb.LogMessage{
Level: level,
Type: typ,
Message: message,
})
}
func (s *server) LogListener(stream pb.Hiddify_LogListenerServer) error {
logSub, _, _ := logObserver.Subscribe()
defer logObserver.UnSubscribe(logSub)
stopch := make(chan int)
go func() {
stream.Recv()
close(stopch)
}()
for {
select {
case <-stream.Context().Done():
break
case <-stopch:
break
case info := <-logSub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}

68
v2/service.go Normal file
View File

@@ -0,0 +1,68 @@
package v2
import (
"context"
"os"
"runtime"
runtimeDebug "runtime/debug"
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/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
)
func Setup(basePath string, workingPath string, tempPath string) {
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()
}
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 parseConfig(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
}

65
v2/warp.go Normal file
View File

@@ -0,0 +1,65 @@
package v2
import (
"context"
"github.com/hiddify/libcore/config"
pb "github.com/hiddify/libcore/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
)
func (s *server) GenerateWarpConfig(ctx context.Context, in *pb.GenerateWarpConfigRequest) (*pb.WarpGenerationResponse, error) {
account, log, wg, err := config.GenerateWarpInfo(in.LicenseKey, in.AccountId, in.AccessToken)
if err != nil {
return nil, err
}
return &pb.WarpGenerationResponse{
Account: &pb.WarpAccount{
AccountId: account.AccountID,
AccessToken: account.AccessToken,
},
Config: &pb.WarpWireguardConfig{
PrivateKey: wg.PrivateKey,
LocalAddressIpv4: wg.LocalAddressIPv4,
LocalAddressIpv6: wg.LocalAddressIPv6,
PeerPublicKey: wg.PeerPublicKey,
},
Log: log,
}, nil
}
// Implement the GetSystemProxyStatus method
func (s *server) 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
}
// Implement the SetSystemProxyEnabled method
func (s *server) SetSystemProxyEnabled(ctx context.Context, in *pb.SetSystemProxyEnabledRequest) (*pb.Response, error) {
err := libbox.NewStandaloneCommandClient().SetSystemProxyEnabled(in.IsEnabled)
if err != nil {
return nil, err
}
if err != nil {
return &pb.Response{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
return &pb.Response{
ResponseCode: pb.ResponseCode_OK,
Message: "",
}, nil
}