diff --git a/.gitignore b/.gitignore index d75a0a6..d56b8bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,9 @@ !/bin/.gitkeep .build .idea - +cert **/*.log .DS_Store -**/*.syso \ No newline at end of file +**/*.syso +node_modules \ No newline at end of file diff --git a/Makefile b/Makefile index 87daac6..2520081 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,15 @@ GOBUILDSRV=CGO_ENABLED=1 go build -ldflags "-s -w" -trimpath -tags $(TAGS) .PHONY: protos protos: - protoc --go_out=config --go-grpc_out=config --proto_path=protos protos/*.proto + protoc --go_out=./ --go-grpc_out=./ --proto_path=hiddifyrpc hiddifyrpc/*.proto + protoc --js_out=import_style=commonjs,binary:./extension/html/rpc/ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./extension/html/rpc/ --proto_path=hiddifyrpc hiddifyrpc/*.proto + npx browserify extension/html/rpc/extension.js >extension/html/rpc.js + lib_install: go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.1 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.1 + npm install headers: go build -buildmode=c-archive -o $(BINDIR)/$(LIBNAME).h ./custom @@ -93,8 +97,6 @@ macos-universal: macos-amd64 macos-arm64 clean: rm $(BINDIR)/* -build_protobuf: - protoc --go_out=. --go-grpc_out=. hiddifyrpc/hiddify.proto diff --git a/cmd/cmd_extension.go b/cmd/cmd_extension.go new file mode 100644 index 0000000..b953b3a --- /dev/null +++ b/cmd/cmd_extension.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + _ "github.com/hiddify/hiddify-core/extension_repository" + "github.com/hiddify/hiddify-core/utils" + v2 "github.com/hiddify/hiddify-core/v2" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/spf13/cobra" + "google.golang.org/grpc" +) + +var extension_id string + +var commandExtension = &cobra.Command{ + Use: "extension", + Short: "extension configuration", + Args: cobra.MaximumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + grpc_server, _ := v2.StartCoreGrpcServer("127.0.0.1:12345") + fmt.Printf("Waiting for CTRL+C to stop\n") + runWebserver(grpc_server) + <-time.After(1 * time.Second) + }, +} + +func init() { + // commandWarp.Flags().StringVarP(&warpKey, "key", "k", "", "warp key") + mainCommand.AddCommand(commandExtension) +} + +func allowCors(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Access-Control-Allow-Origin", "*") + resp.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + resp.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if req.Method == "OPTIONS" { + resp.WriteHeader(http.StatusOK) + return + } +} + +func runWebserver(grpcServer *grpc.Server) { + // Context for cancellation + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Channels to signal termination + grpcTerminated := make(chan struct{}) + grpcWebTerminated := make(chan struct{}) + + // Specify the directory to serve static files + dir := "./extension/html/" + + // Wrapping gRPC server with grpc-web + grpcWeb := grpcweb.WrapServer(grpcServer) + + // HTTP multiplexer + mux := http.NewServeMux() + mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { + allowCors(resp, req) + if grpcWeb.IsGrpcWebRequest(req) || grpcWeb.IsAcceptableGrpcCorsRequest(req) { + grpcWeb.ServeHTTP(resp, req) + } else { + http.DefaultServeMux.ServeHTTP(resp, req) + } + }) + + // File server for static files + fs := http.FileServer(http.Dir(dir)) + http.Handle("/", http.StripPrefix("/", fs)) + + // HTTP server for grpc-web + rpcWebServer := &http.Server{ + Handler: mux, + Addr: ":12346", + } + log.Println("Serving grpc-web from https://localhost:12346/") + + // Add a goroutine for the grpc-web server + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + utils.GenerateCertificate("cert/server-cert.pem", "cert/server-key.pem", true, true) + if err := rpcWebServer.ListenAndServeTLS("cert/server-cert.pem", "cert/server-key.pem"); err != nil && err != http.ErrServerClosed { + // if err := rpcWebServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("Web server (gRPC-web) shutdown with error: %s", err) + } + grpcServer.Stop() + close(grpcWebTerminated) // Server terminated + }() + + // Signal handling to gracefully shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + select { + case <-ctx.Done(): // Context canceled + log.Println("Context canceled, shutting down servers...") + case sig := <-sigChan: // OS signal received + log.Printf("Received signal: %s, shutting down servers...", sig) + case <-grpcTerminated: // Unexpected gRPC termination + log.Println("gRPC server terminated unexpectedly") + case <-grpcWebTerminated: // Unexpected gRPC-web termination + log.Println("gRPC-web server terminated unexpectedly") + } + + // Graceful shutdown of the servers + if err := rpcWebServer.Shutdown(ctx); err != nil { + log.Printf("gRPC-web server shutdown with error: %s", err) + } + <-grpcWebTerminated + + // Ensure all routines finish + wg.Wait() + log.Println("Server shutdown complete") +} diff --git a/cmd/cmd_gen_cert.go b/cmd/cmd_gen_cert.go index 3d62a47..cd78289 100644 --- a/cmd/cmd_gen_cert.go +++ b/cmd/cmd_gen_cert.go @@ -11,11 +11,11 @@ var commandGenerateCertification = &cobra.Command{ Use: "gen-cert", Short: "Generate certification for web server", Run: func(cmd *cobra.Command, args []string) { - err := os.MkdirAll("cert", 0644) + err := os.MkdirAll("cert", 0o644) if err != nil { panic("Error: " + err.Error()) } - utils.GenerateCertificate("cert/server-cert.pem", "cert/server-key.pem", true) - utils.GenerateCertificate("cert/client-cert.pem", "cert/client-key.pem", false) + utils.GenerateCertificate("cert/server-cert.pem", "cert/server-key.pem", true, true) + utils.GenerateCertificate("cert/client-cert.pem", "cert/client-key.pem", false, true) }, } diff --git a/extension/extension.go b/extension/extension.go new file mode 100644 index 0000000..55b90fa --- /dev/null +++ b/extension/extension.go @@ -0,0 +1,86 @@ +package extension + +import ( + "fmt" + "log" + + "github.com/hiddify/hiddify-core/extension/ui_elements" + pb "github.com/hiddify/hiddify-core/hiddifyrpc" +) + +var ( + extensionsMap = make(map[string]*Extension) + extensionStatusMap = make(map[string]bool) +) + +type Extension interface { + GetTitle() string + GetDescription() string + GetUI() ui_elements.Form + SubmitData(data map[string]string) error + Cancel() error + Stop() error + UpdateUI(form ui_elements.Form) error + init(id string) + getQueue() chan *pb.ExtensionResponse + getId() string +} + +type BaseExtension struct { + id string + // responseStream grpc.ServerStreamingServer[pb.ExtensionResponse] + queue chan *pb.ExtensionResponse +} + +// func (b *BaseExtension) mustEmbdedBaseExtension() { +// } + +func (b *BaseExtension) init(id string) { + b.id = id + b.queue = make(chan *pb.ExtensionResponse, 1) +} + +func (b *BaseExtension) getQueue() chan *pb.ExtensionResponse { + return b.queue +} + +func (b *BaseExtension) getId() string { + return b.id +} + +func (p *BaseExtension) UpdateUI(form ui_elements.Form) error { + p.queue <- &pb.ExtensionResponse{ + ExtensionId: p.id, + Type: pb.ExtensionResponseType_UPDATE_UI, + JsonUi: form.ToJSON(), + } + return nil +} + +func (p *BaseExtension) ShowDialog(form ui_elements.Form) error { + p.queue <- &pb.ExtensionResponse{ + ExtensionId: p.id, + Type: pb.ExtensionResponseType_SHOW_DIALOG, + JsonUi: form.ToJSON(), + } + // log.Printf("Updated UI for extension %s: %s", err, p.id) + return nil +} + +func RegisterExtension(id string, extension Extension) error { + if _, ok := extensionsMap[id]; ok { + err := fmt.Errorf("Extension with ID %s already exists", id) + log.Fatal(err) + return err + } + if val, ok := extensionStatusMap[id]; ok && !val { + err := fmt.Errorf("Extension with ID %s is not enabled", id) + log.Fatal(err) + return err + } + extension.init(id) + + fmt.Printf("Registered extension: %+v\n", extension) + extensionsMap[id] = &extension + return nil +} diff --git a/extension/extension_host.go b/extension/extension_host.go new file mode 100644 index 0000000..09437f9 --- /dev/null +++ b/extension/extension_host.go @@ -0,0 +1,139 @@ +package extension + +import ( + "context" + "fmt" + "log" + + pb "github.com/hiddify/hiddify-core/hiddifyrpc" + "google.golang.org/grpc" +) + +type ExtensionHostService struct { + pb.UnimplementedExtensionHostServiceServer +} + +func (ExtensionHostService) ListExtensions(ctx context.Context, empty *pb.Empty) (*pb.ExtensionList, error) { + extensionList := &pb.ExtensionList{ + Extensions: make([]*pb.Extension, 0), + } + + for _, extension := range extensionsMap { + extensionList.Extensions = append(extensionList.Extensions, &pb.Extension{ + Id: (*extension).getId(), + Title: (*extension).GetTitle(), + Description: (*extension).GetDescription(), + }) + } + return extensionList, nil +} + +func (e ExtensionHostService) Connect(req *pb.ExtensionRequest, stream grpc.ServerStreamingServer[pb.ExtensionResponse]) error { + // Get the extension from the map using the Extension ID + if extension, ok := extensionsMap[req.GetExtensionId()]; ok { + + log.Printf("Connecting stream for extension %s", req.GetExtensionId()) + log.Printf("Extension data: %+v", extension) + // Handle loading the UI for the extension + // Call extension-specific logic to generate UI data + // if err := platform.connect(stream); err != nil { + // log.Printf("Error connecting stream for extension %s: %v", req.GetExtensionId(), err) + // } + if err := (*extension).UpdateUI((*extension).GetUI()); err != nil { + log.Printf("Error updating UI for extension %s: %v", req.GetExtensionId(), err) + } + // info := <-platform.queue + + // stream.Send(info) + // (*platform.extension).SubmitData(map[string]string{}) + // log.Printf("Extension info: %+v", info) + // // Handle submitting data to the extension + // case pb.ExtensionRequestType_SUBMIT_DATA: + // // Handle submitting data to the extension + // // Process the provided data + // err := extension.SubmitData(req.GetData()) + // if err != nil { + // log.Printf("Error submitting data for extension %s: %v", req.GetExtensionId(), err) + // // continue + // } + + // case hiddifyrpc.ExtensionRequestType_CANCEL: + // // Handle canceling the current operation in the extension + // extension.Stop() + // log.Printf("Operation canceled for extension %s", req.GetExtensionId()) + + // default: + // log.Printf("Unknown request type: %v", req.GetType()) + // } + + for { + select { + case <-stream.Context().Done(): + return nil + case info := <-(*extension).getQueue(): + stream.Send(info) + if info.GetType() == pb.ExtensionResponseType_END { + return nil + } + } + } + + // break + // case <-stopCh: + // break + // // case info := <-sub: + // // stream.Send(&info) + // case <-time.After(1000 * time.Millisecond): + // } + + // extension := extensionsMap[data.GetExtensionId()] + // ui := extension.GetUI(data.Data) + + // return &pb.UI{ + // ExtensionId: data.GetExtensionId(), + // JsonUi: ui.ToJSON(), + // }, nil + } else { + log.Printf("Extension with ID %s not found", req.GetExtensionId()) + return fmt.Errorf("Extension with ID %s not found", req.GetExtensionId()) + } +} + +func (e ExtensionHostService) SubmitForm(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) { + if extension, ok := extensionsMap[req.GetExtensionId()]; ok { + (*extension).SubmitData(req.GetData()) + + return &pb.ExtensionActionResult{ + ExtensionId: req.ExtensionId, + Code: pb.ResponseCode_OK, + Message: "Success", + }, nil + } + return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId()) +} + +func (e ExtensionHostService) Cancel(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) { + if extension, ok := extensionsMap[req.GetExtensionId()]; ok { + (*extension).Cancel() + + return &pb.ExtensionActionResult{ + ExtensionId: req.ExtensionId, + Code: pb.ResponseCode_OK, + Message: "Success", + }, nil + } + return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId()) +} + +func (e ExtensionHostService) Stop(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) { + if extension, ok := extensionsMap[req.GetExtensionId()]; ok { + (*extension).Stop() + + return &pb.ExtensionActionResult{ + ExtensionId: req.ExtensionId, + Code: pb.ResponseCode_OK, + Message: "Success", + }, nil + } + return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId()) +} diff --git a/extension/html/index.html b/extension/html/index.html new file mode 100644 index 0000000..431c337 --- /dev/null +++ b/extension/html/index.html @@ -0,0 +1,52 @@ + + +
+ + +