From 6696e6fb9ebb377663dfd81b9d8a55cced9bc4b6 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Wed, 18 Sep 2024 23:41:13 +0100 Subject: [PATCH] feat(cmd)_: status-backend --- cmd/status-backend/main.go | 29 ++++ cmd/statusd/main.go | 1 + cmd/statusd/server/endpoints.go | 101 ++++++++++++ .../server/parse-api/endpoints_template.txt | 21 +++ cmd/statusd/server/parse-api/main.go | 148 ++++++++++++++++++ cmd/statusd/server/signals_server.go | 97 ++++++++++-- mobile/status.go | 1 + 7 files changed, 386 insertions(+), 12 deletions(-) create mode 100644 cmd/status-backend/main.go create mode 100644 cmd/statusd/server/endpoints.go create mode 100644 cmd/statusd/server/parse-api/endpoints_template.txt create mode 100644 cmd/statusd/server/parse-api/main.go diff --git a/cmd/status-backend/main.go b/cmd/status-backend/main.go new file mode 100644 index 000000000..d06f0b6d1 --- /dev/null +++ b/cmd/status-backend/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/cmd/statusd/server" +) + +var ( + address = flag.String("address", "", "host:port to listen") + logger = log.New("package", "status-go/cmd/status-backend") +) + +func main() { + + srv := server.NewServer() + srv.Setup() + + err := srv.Listen(*address) + if err != nil { + logger.Error("failed to start server", "error", err) + return + } + + log.Info("server started", "address", srv.Address()) + srv.RegisterMobileAPI() + srv.Serve() +} diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index d22ace42f..fe50b6c01 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -178,6 +178,7 @@ func main() { logger.Error("failed to start server", "error", err) return } + go srv.Serve() log.Info("server started", "address", srv.Address()) defer func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) diff --git a/cmd/statusd/server/endpoints.go b/cmd/statusd/server/endpoints.go new file mode 100644 index 000000000..3bd7f0e88 --- /dev/null +++ b/cmd/statusd/server/endpoints.go @@ -0,0 +1,101 @@ +package server + +import "github.com/status-im/status-go/mobile" + +var EndpointsWithResponse = []func(string) string { + statusgo.InitializeApplication, + statusgo.ExtractGroupMembershipSignatures, + statusgo.SignGroupMembership, + statusgo.ValidateNodeConfig, + statusgo.CallRPC, + statusgo.CallPrivateRPC, + statusgo.CreateAccountAndLogin, + statusgo.LoginAccount, + statusgo.RestoreAccountAndLogin, + statusgo.InitKeystore, + statusgo.SignMessage, + statusgo.HashTypedData, + statusgo.HashTypedDataV4, + statusgo.Recover, + statusgo.HashTransaction, + statusgo.HashMessage, + statusgo.StartCPUProfile, + statusgo.WriteHeapProfile, + statusgo.AddPeer, + statusgo.SignHash, + statusgo.GenerateAlias, + statusgo.IsAlias, + statusgo.Identicon, + statusgo.EmojiHash, + statusgo.ColorHash, + statusgo.ColorID, + statusgo.ValidateMnemonic, + statusgo.DecompressPublicKey, + statusgo.CompressPublicKey, + statusgo.SerializeLegacyKey, + statusgo.GetPasswordStrength, + statusgo.GetPasswordStrengthScore, + statusgo.GetConnectionStringForBeingBootstrapped, + statusgo.GetConnectionStringForBootstrappingAnotherDevice, + statusgo.GetConnectionStringForExportingKeypairsKeystores, + statusgo.ValidateConnectionString, + statusgo.DecodeParameters, + statusgo.HexToNumber, + statusgo.NumberToHex, + statusgo.Sha3, + statusgo.Utf8ToHex, + statusgo.HexToUtf8, + statusgo.CheckAddressChecksum, + statusgo.IsAddress, + statusgo.ToChecksumAddress, + statusgo.DeserializeAndCompressKey, + statusgo.InitLogging, + statusgo.ToggleCentralizedMetrics, + statusgo.AddCentralizedMetric, +} + +var EndpointsNoRequest = []func() string { + statusgo.GetNodeConfig, + statusgo.ResetChainData, + statusgo.Logout, + statusgo.StopCPUProfiling, + statusgo.StartLocalNotifications, + statusgo.StopLocalNotifications, + statusgo.ExportNodeLogs, + statusgo.ImageServerTLSCert, + statusgo.Fleets, + statusgo.LocalPairingPreflightOutboundCheck, + statusgo.StartSearchForLocalPairingPeers, + statusgo.GetRandomMnemonic, + statusgo.CentralizedMetricsInfo, +} + +var EndpointsUnsupported = []string { + "VerifyAccountPassword", + "VerifyDatabasePassword", + "MigrateKeyStoreDir", + "DeleteMultiaccount", + "DeleteImportedKey", + "SignTypedData", + "SignTypedDataV4", + "SendTransactionWithChainID", + "SendTransaction", + "SendTransactionWithSignature", + "ConnectionChange", + "AppStateChange", + "SetMobileSignalHandler", + "SetSignalEventCallback", + "MultiformatSerializePublicKey", + "MultiformatDeserializePublicKey", + "ExportUnencryptedDatabase", + "ImportUnencryptedDatabase", + "ChangeDatabasePassword", + "ConvertToKeycardAccount", + "ConvertToRegularAccount", + "SwitchFleet", + "InputConnectionStringForBootstrapping", + "InputConnectionStringForBootstrappingAnotherDevice", + "InputConnectionStringForImportingKeypairsKeystores", + "EncodeTransfer", + "EncodeFunctionCall", +} \ No newline at end of file diff --git a/cmd/statusd/server/parse-api/endpoints_template.txt b/cmd/statusd/server/parse-api/endpoints_template.txt new file mode 100644 index 000000000..896d94b2a --- /dev/null +++ b/cmd/statusd/server/parse-api/endpoints_template.txt @@ -0,0 +1,21 @@ +package server + +import "github.com/status-im/status-go/mobile" + +var EndpointsWithResponse = []func(string) string { + {{- range .FunctionsWithResp }} + {{ $.PackageName }}.{{ . }}, +{{- end }} +} + +var EndpointsNoRequest = []func() string { + {{- range .FunctionsNoArgs }} + {{ $.PackageName }}.{{ . }}, +{{- end }} +} + +var EndpointsUnsupported = []string { + {{- range .UnsupportedEndpoints }} + "{{ . }}", +{{- end }} +} \ No newline at end of file diff --git a/cmd/statusd/server/parse-api/main.go b/cmd/statusd/server/parse-api/main.go new file mode 100644 index 000000000..cf8256969 --- /dev/null +++ b/cmd/statusd/server/parse-api/main.go @@ -0,0 +1,148 @@ +//go:generate go run main.go + +package main + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + "text/template" +) + +const ( + inputFilePath = "../../../../mobile/status.go" + templateFilePath = "./endpoints_template.txt" + outputFilePath = "../endpoints.go" +) + +var ( + // Regular expressions extracted as global variables + publicFunc = regexp.MustCompile(`func\s+([A-Z]\w+)\(.*\).*\{`) + publicFuncWithRespPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\((\w|\s)+\)\s+string\s+\{$`) + publicFuncNoArgsPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\(\)\s+string\s+\{$`) + funcNamePattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\(`) +) + +type TemplateData struct { + PackageName string + FunctionsWithResp []string + FunctionsNoArgs []string + UnsupportedEndpoints []string +} + +func main() { + // Open the Go source file + file, err := os.Open(inputFilePath) + if err != nil { + fmt.Printf("Failed to open file: %s\n", err) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var publicFunctionsWithResp []string + var publicFunctionsNoArgs []string + var unsupportedFunctions []string + var isDeprecated bool + var packageName string + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Detect package name + if strings.HasPrefix(line, "package ") { + packageName = strings.TrimPrefix(line, "package ") + } + + // Check for deprecation comment + if isDeprecatedComment(line) { + isDeprecated = true + continue + } + + if !publicFunc.MatchString(line) { + continue + } + + if isDeprecated { + isDeprecated = false + continue + } + + functionName := extractFunctionName(line) + + switch { + case isPublicFunctionWithResp(line): + publicFunctionsWithResp = append(publicFunctionsWithResp, functionName) + continue + case isPublicFunctionNoArgs(line): + publicFunctionsNoArgs = append(publicFunctionsNoArgs, functionName) + continue + default: + unsupportedFunctions = append(unsupportedFunctions, functionName) + } + } + + if err := scanner.Err(); err != nil { + fmt.Printf("Error reading file: %s\n", err) + return + } + + // Prepare the template data + data := TemplateData{ + PackageName: packageName, + FunctionsWithResp: publicFunctionsWithResp, + FunctionsNoArgs: publicFunctionsNoArgs, + UnsupportedEndpoints: unsupportedFunctions, + } + + // Load and parse the template + tmpl, err := template.ParseFiles(templateFilePath) + if err != nil { + fmt.Printf("Failed to parse template file: %s\n", err) + return + } + + // Create the output file + outputFile, err := os.Create(outputFilePath) + if err != nil { + fmt.Printf("Failed to create output file: %s\n", err) + return + } + defer outputFile.Close() + + // Execute the template and write the result to the output file + err = tmpl.Execute(outputFile, data) + if err != nil { + fmt.Printf("Failed to execute template: %s\n", err) + return + } + + fmt.Println("Generated endpoints file:", outputFilePath) +} + +// Function to check if a line contains a public function with a response of string +func isPublicFunctionWithResp(line string) bool { + return publicFuncWithRespPattern.MatchString(line) +} + +// Function to check if a line contains a public function with not arguments and a response of string +func isPublicFunctionNoArgs(line string) bool { + return publicFuncNoArgsPattern.MatchString(line) +} + +// Function to extract the public function name from a line +func extractFunctionName(line string) string { + matches := funcNamePattern.FindStringSubmatch(line) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +// Function to check if a comment indicates a deprecated function +func isDeprecatedComment(line string) bool { + return strings.Contains(line, "// Deprecated:") +} diff --git a/cmd/statusd/server/signals_server.go b/cmd/statusd/server/signals_server.go index 661c58209..f66c6dc6a 100644 --- a/cmd/statusd/server/signals_server.go +++ b/cmd/statusd/server/signals_server.go @@ -3,8 +3,13 @@ package server import ( "context" "errors" + "fmt" + "io" "net" "net/http" + "reflect" + "runtime" + "strings" "sync" "time" @@ -17,6 +22,8 @@ import ( type Server struct { server *http.Server + listener net.Listener + mux *http.ServeMux lock sync.Mutex connections map[*websocket.Conn]struct{} address string @@ -58,27 +65,28 @@ func (s *Server) Listen(address string) error { ReadHeaderTimeout: 5 * time.Second, } - mux := http.NewServeMux() - mux.HandleFunc("/signals", s.signals) - s.server.Handler = mux + s.mux = http.NewServeMux() + s.mux.HandleFunc("/signals", s.signals) + s.server.Handler = s.mux - listener, err := net.Listen("tcp", address) + var err error + s.listener, err = net.Listen("tcp", address) if err != nil { return err } - s.address = listener.Addr().String() - - go func() { - err := s.server.Serve(listener) - if !errors.Is(err, http.ErrServerClosed) { - log.Error("signals server closed with error: %w", err) - } - }() + s.address = s.listener.Addr().String() return nil } +func (s *Server) Serve() { + err := s.server.Serve(s.listener) + if !errors.Is(err, http.ErrServerClosed) { + log.Error("signals server closed with error: %w", err) + } +} + func (s *Server) Stop(ctx context.Context) { for connection := range s.connections { err := connection.Close() @@ -115,3 +123,68 @@ func (s *Server) signals(w http.ResponseWriter, r *http.Request) { s.connections[connection] = struct{}{} } + +func (s *Server) addEndpointWithResponse(handler func(string) string) { + endpoint := endpointName(functionName(handler)) + log.Info("adding endpoint", "name", endpoint) + s.mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + request, err := io.ReadAll(r.Body) + if err != nil { + log.Error("failed to read request: %w", err) + return + } + + response := handler(string(request)) + + _, err = w.Write([]byte(response)) + if err != nil { + log.Error("failed to write response: %w", err) + } + }) +} + +func (s *Server) addEndpointNoRequest(handler func() string) { + endpoint := endpointName(functionName(handler)) + log.Info("adding endpoint", "name", endpoint) + s.mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + response := handler() + + _, err := w.Write([]byte(response)) + if err != nil { + log.Error("failed to write response: %w", err) + } + }) +} + +func (s *Server) addUnsupportedEndpoint(name string) { + endpoint := endpointName(name) + log.Info("marking unsupported endpoint", "name", endpoint) + s.mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + }) +} + +func (s *Server) RegisterMobileAPI() { + for _, endpoint := range EndpointsWithResponse { + s.addEndpointWithResponse(endpoint) + } + for _, endpoint := range EndpointsNoRequest { + s.addEndpointNoRequest(endpoint) + } + for _, endpoint := range EndpointsUnsupported { + s.addUnsupportedEndpoint(endpoint) + } +} + +func functionName(fn any) string { + fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() + parts := strings.Split(fullName, "/") + lastPart := parts[len(parts)-1] + nameParts := strings.Split(lastPart, ".") + return nameParts[len(nameParts)-1] +} + +func endpointName(functionName string) string { + const base = "statusgo" + return fmt.Sprintf("/%s/%s", base, functionName) +} diff --git a/mobile/status.go b/mobile/status.go index 6b22db8dc..0598f609a 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -99,6 +99,7 @@ func initializeApplication(requestJSON string) string { return string(data) } +// Deprecated: Use InitializeApplication instead. func OpenAccounts(datadir string) string { return logAndCallString(openAccounts, datadir) }