diff --git a/Makefile b/Makefile index 10816b3ae..4d4f056ec 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,13 @@ spiff-workflow: build/bin/spiff-workflow status-cli: ##@build Build status-cli to send messages status-cli: build/bin/status-cli +status-backend: ##@build Build status-backend to run status-go as HTTP server +status-backend: build/bin/status-backend + +run-status-backend: PORT ?= 0 +run-status-backend: ##@run Start status-backend server listening to localhost:PORT + go run ./cmd/status-backend --address localhost:${PORT} + statusd-prune-docker-image: SHELL := /bin/sh statusd-prune-docker-image: ##@statusd-prune Build statusd-prune docker image @echo "Building docker image for ststusd-prune..." diff --git a/cmd/status-backend/README.md b/cmd/status-backend/README.md new file mode 100644 index 000000000..4e04dee5d --- /dev/null +++ b/cmd/status-backend/README.md @@ -0,0 +1,155 @@ +# Description + +Welcome to `status-backend`. This is a tool for debugging and testing `status-go`. +In contrast to existing `statusd` and `status-cli`, the `status-backend` exposes full status-go API through HTTP. + +This allows to communicate with status-go through HTTP the same way as `status-desktop` and `status-mobile` do, including: +- create account +- restore account +- login +- logout +- start messenger +- start wallet +- subscribe to status-go signals +- etc. + +# status-go API + +## Public methods in `./mobile/status.go` + +Only specific function signatures are currently supported: + - `func(string) string` - 1 argument, 1 return + - `func() string` - 0 argument, 1 return + +### Unsupported methods + +Attempt to call any other functions will return `501: Not Implemented` HTTP code. +For example, [`VerifyAccountPassword`](https://github.com/status-im/status-go/blob/669256095e16d953ca1af4954b90ca2ae65caa2f/mobile/status.go#L275-L277) has 3 arguments: +```go +func VerifyAccountPassword(keyStoreDir, address, password string) string { + return logAndCallString(verifyAccountPassword, keyStoreDir, address, password) +} +``` + +Later, as needed, a V2 of these functions will be introduced. V2 will have a single JSON argument composing all args in 1. +For example, https://github.com/status-im/status-go/pull/5865 fixes some of these. + +### Deprecated methods + +Deprecated methods will have `Deprecation: true` HTTP header. + +## Signals in `./signal` + +Each signal has [this structure](https://github.com/status-im/status-go/blob/c9b777a2186364b8f394ad65bdb18b128ceffa70/signal/signals.go#L30-L33): +```go +// Envelope is a general signal sent upward from node to RN app +type Envelope struct { + Type string `json:"type"` + Event interface{} `json:"event"` +} +``` + +List of possible events can be found in `./signal/event_*.go` files. + +For example, `node.login` event is defined [here](https://github.com/status-im/status-go/blob/6bcf5f1289f9160168574290cbd6f90dede3f8f6/signal/events_node.go#L27-L28): +```go +const ( + // EventLoggedIn is once node was injected with user account and ready to be used. + EventLoggedIn = "node.login" +) +``` + +And the structure of this event is [defined in the same file](https://github.com/status-im/status-go/blob/6bcf5f1289f9160168574290cbd6f90dede3f8f6/signal/events_node.go#L36-L42): +```go +// NodeLoginEvent returns the result of the login event +type NodeLoginEvent struct { + Error string `json:"error,omitempty"` + Settings *settings.Settings `json:"settings,omitempty"` + Account *multiaccounts.Account `json:"account,omitempty"` + EnsUsernames json.RawMessage `json:"ensUsernames,omitempty"` +} +``` + + +So the signal for `node.login` event will look like this (with corresponding data): +```json +{ + "type": "node.login", + "event": { + "error": "", + "settings": {}, + "account": {}, + "endUsernames": {} + } +} +``` + +## Services in `./services/**/api.go` + +Services are registered in go-ethereum JSON-RPC server. To call such method, send request to `statusgo/CallRPC` endpoint. + +For example: +```http request +### Send Contact Request +POST http://localhost:12345/statusgo/CallRPC + +{ + "jsonrpc": "2.0", + "method": "wakuext_sendContactRequest", + "params": [ + { + "id": "0x048f0b885010783429c2298b916e24b3c01f165e55fe8f98fce63df0a55ade80089f512943d4fde5f8c7211f1a87b267a85cbcb3932eb2e4f88aa4ca3918f97541", + "message": "Hi, Alice!" + } + ] +} +``` + +### Notes + +1. In this case, there's no limitation to the number of arguments, comparing to `mobile/status.go`, so ll method are supported. +2. Deprecated methods won't have a corresponding `Deprecated: true` + +# Usage + +Start the app with the address to listen to: +```shell +status-backend --address localhost:12345 +``` + +Or just use the root repo Makefile command: +```shell +make run-status-backend PORT=12345 +``` + +Access the exposed API with any HTTP client you prefer: +- From your IDE: + - [JetBrains](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) + - [VS Code](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) +- From UI client: + - [Postman](https://learning.postman.com/docs/getting-started/first-steps/sending-the-first-request/) + - [Insomnia](https://docs.insomnia.rest/insomnia/send-your-first-request) +- From command line: + - [Curl](https://curl.se/docs/httpscripting.html) +- From your script: + - [Python](https://pypi.org/project/requests/) + - [Go](https://pkg.go.dev/net/http) + +# Simple flows + +In most cases to start testing you'll need some boilerplate. Below are the simple call flows for common cases. + +## Create account and login + +1. `InitializeApplication` +2. `CreateAccountAndLogin` +3. `wakuext_startMessenger` +4. `wallet_startWallet` +5. `settings_getSettings` (temporary workaround, otherwise settings don't get saved into DB) + +## Login into account + +1. `InitializeApplication` +2. `LoginAccount` +3. `wakuext_startMessenger` +4. `wallet_startWallet` \ No newline at end of file diff --git a/cmd/status-backend/main.go b/cmd/status-backend/main.go new file mode 100644 index 000000000..0b2be9c31 --- /dev/null +++ b/cmd/status-backend/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + stdlog "log" + "os" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/cmd/statusd/server" + "github.com/status-im/status-go/logutils" +) + +var ( + address = flag.String("address", "", "host:port to listen") + logger = log.New("package", "status-go/cmd/status-backend") +) + +func init() { + logSettings := logutils.LogSettings{ + Enabled: true, + MobileSystem: false, + Level: "INFO", + } + colors := terminal.IsTerminal(int(os.Stdin.Fd())) + if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil { + stdlog.Fatalf("failed to initialize log: %v", err) + } +} + +func main() { + flag.Parse() + + 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..8a1117bc5 --- /dev/null +++ b/cmd/statusd/server/endpoints.go @@ -0,0 +1,120 @@ +// Code generated by parse-api/main.go. DO NOT EDIT. +// source: parse-api/main.go + +package server + +import statusgo "github.com/status-im/status-go/mobile" + +var EndpointsWithRequest = map[string]func(string) string{ + "/statusgo/InitializeApplication": statusgo.InitializeApplication, + "/statusgo/OpenAccounts": statusgo.OpenAccounts, + "/statusgo/ExtractGroupMembershipSignatures": statusgo.ExtractGroupMembershipSignatures, + "/statusgo/SignGroupMembership": statusgo.SignGroupMembership, + "/statusgo/ValidateNodeConfig": statusgo.ValidateNodeConfig, + "/statusgo/CallRPC": statusgo.CallRPC, + "/statusgo/CallPrivateRPC": statusgo.CallPrivateRPC, + "/statusgo/CreateAccountAndLogin": statusgo.CreateAccountAndLogin, + "/statusgo/LoginAccount": statusgo.LoginAccount, + "/statusgo/RestoreAccountAndLogin": statusgo.RestoreAccountAndLogin, + "/statusgo/InitKeystore": statusgo.InitKeystore, + "/statusgo/SignMessage": statusgo.SignMessage, + "/statusgo/HashTypedData": statusgo.HashTypedData, + "/statusgo/HashTypedDataV4": statusgo.HashTypedDataV4, + "/statusgo/Recover": statusgo.Recover, + "/statusgo/HashTransaction": statusgo.HashTransaction, + "/statusgo/HashMessage": statusgo.HashMessage, + "/statusgo/StartCPUProfile": statusgo.StartCPUProfile, + "/statusgo/WriteHeapProfile": statusgo.WriteHeapProfile, + "/statusgo/AddPeer": statusgo.AddPeer, + "/statusgo/SignHash": statusgo.SignHash, + "/statusgo/GenerateAlias": statusgo.GenerateAlias, + "/statusgo/IsAlias": statusgo.IsAlias, + "/statusgo/Identicon": statusgo.Identicon, + "/statusgo/EmojiHash": statusgo.EmojiHash, + "/statusgo/ColorHash": statusgo.ColorHash, + "/statusgo/ColorID": statusgo.ColorID, + "/statusgo/ValidateMnemonic": statusgo.ValidateMnemonic, + "/statusgo/DecompressPublicKey": statusgo.DecompressPublicKey, + "/statusgo/CompressPublicKey": statusgo.CompressPublicKey, + "/statusgo/SerializeLegacyKey": statusgo.SerializeLegacyKey, + "/statusgo/GetPasswordStrength": statusgo.GetPasswordStrength, + "/statusgo/GetPasswordStrengthScore": statusgo.GetPasswordStrengthScore, + "/statusgo/GetConnectionStringForBeingBootstrapped": statusgo.GetConnectionStringForBeingBootstrapped, + "/statusgo/GetConnectionStringForBootstrappingAnotherDevice": statusgo.GetConnectionStringForBootstrappingAnotherDevice, + "/statusgo/GetConnectionStringForExportingKeypairsKeystores": statusgo.GetConnectionStringForExportingKeypairsKeystores, + "/statusgo/ValidateConnectionString": statusgo.ValidateConnectionString, + "/statusgo/DecodeParameters": statusgo.DecodeParameters, + "/statusgo/HexToNumber": statusgo.HexToNumber, + "/statusgo/NumberToHex": statusgo.NumberToHex, + "/statusgo/Sha3": statusgo.Sha3, + "/statusgo/Utf8ToHex": statusgo.Utf8ToHex, + "/statusgo/HexToUtf8": statusgo.HexToUtf8, + "/statusgo/CheckAddressChecksum": statusgo.CheckAddressChecksum, + "/statusgo/IsAddress": statusgo.IsAddress, + "/statusgo/ToChecksumAddress": statusgo.ToChecksumAddress, + "/statusgo/DeserializeAndCompressKey": statusgo.DeserializeAndCompressKey, + "/statusgo/InitLogging": statusgo.InitLogging, + "/statusgo/ToggleCentralizedMetrics": statusgo.ToggleCentralizedMetrics, + "/statusgo/AddCentralizedMetric": statusgo.AddCentralizedMetric, +} + +var EndpointsWithoutRequest = map[string]func() string{ + "/statusgo/GetNodeConfig": statusgo.GetNodeConfig, + "/statusgo/ResetChainData": statusgo.ResetChainData, + "/statusgo/Logout": statusgo.Logout, + "/statusgo/StopCPUProfiling": statusgo.StopCPUProfiling, + "/statusgo/StartLocalNotifications": statusgo.StartLocalNotifications, + "/statusgo/StopLocalNotifications": statusgo.StopLocalNotifications, + "/statusgo/ExportNodeLogs": statusgo.ExportNodeLogs, + "/statusgo/ImageServerTLSCert": statusgo.ImageServerTLSCert, + "/statusgo/Fleets": statusgo.Fleets, + "/statusgo/LocalPairingPreflightOutboundCheck": statusgo.LocalPairingPreflightOutboundCheck, + "/statusgo/StartSearchForLocalPairingPeers": statusgo.StartSearchForLocalPairingPeers, + "/statusgo/GetRandomMnemonic": statusgo.GetRandomMnemonic, + "/statusgo/CentralizedMetricsInfo": statusgo.CentralizedMetricsInfo, +} + +var EndpointsUnsupported = []string{ + "/statusgo/VerifyAccountPassword", + "/statusgo/VerifyDatabasePassword", + "/statusgo/MigrateKeyStoreDir", + "/statusgo/Login", + "/statusgo/LoginWithConfig", + "/statusgo/SaveAccountAndLogin", + "/statusgo/DeleteMultiaccount", + "/statusgo/DeleteImportedKey", + "/statusgo/SaveAccountAndLoginWithKeycard", + "/statusgo/LoginWithKeycard", + "/statusgo/SignTypedData", + "/statusgo/SignTypedDataV4", + "/statusgo/SendTransactionWithChainID", + "/statusgo/SendTransaction", + "/statusgo/SendTransactionWithSignature", + "/statusgo/ConnectionChange", + "/statusgo/AppStateChange", + "/statusgo/SetMobileSignalHandler", + "/statusgo/SetSignalEventCallback", + "/statusgo/MultiformatSerializePublicKey", + "/statusgo/MultiformatDeserializePublicKey", + "/statusgo/ExportUnencryptedDatabase", + "/statusgo/ImportUnencryptedDatabase", + "/statusgo/ChangeDatabasePassword", + "/statusgo/ConvertToKeycardAccount", + "/statusgo/ConvertToRegularAccount", + "/statusgo/SwitchFleet", + "/statusgo/GenerateImages", + "/statusgo/InputConnectionStringForBootstrapping", + "/statusgo/InputConnectionStringForBootstrappingAnotherDevice", + "/statusgo/InputConnectionStringForImportingKeypairsKeystores", + "/statusgo/EncodeTransfer", + "/statusgo/EncodeFunctionCall", +} + +var EndpointsDeprecated = map[string]struct{}{ + "/statusgo/OpenAccounts": {}, + "/statusgo/Login": {}, + "/statusgo/LoginWithConfig": {}, + "/statusgo/SaveAccountAndLogin": {}, + "/statusgo/SaveAccountAndLoginWithKeycard": {}, + "/statusgo/LoginWithKeycard": {}, +} \ 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..0cbafd32f --- /dev/null +++ b/cmd/statusd/server/parse-api/endpoints_template.txt @@ -0,0 +1,30 @@ +// Code generated by parse-api/main.go. DO NOT EDIT. +// source: parse-api/main.go + +package server + +import statusgo "github.com/status-im/status-go/mobile" + +var EndpointsWithRequest = map[string]func(string) string{ + {{- range .FunctionsWithResp }} + "/{{ $.PackageName }}/{{ . }}": {{ $.PackageName }}.{{ . }}, +{{- end }} +} + +var EndpointsWithoutRequest = map[string]func() string{ + {{- range .FunctionsNoArgs }} + "/{{ $.PackageName }}/{{ . }}": {{ $.PackageName }}.{{ . }}, +{{- end }} +} + +var EndpointsUnsupported = []string{ + {{- range .UnsupportedEndpoints }} + "/{{ $.PackageName }}/{{ . }}", +{{- end }} +} + +var EndpointsDeprecated = map[string]struct{}{ + {{- range .DeprecatedEndpoints }} + "/{{ $.PackageName }}/{{ . }}": {}, +{{- 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..92181cfa8 --- /dev/null +++ b/cmd/statusd/server/parse-api/main.go @@ -0,0 +1,167 @@ +//go:generate go run main.go + +/* +This script generates a Go file with a list of supported endpoints based on the public functions in `mobile/status.go`. +The output has 3 sections: +- Endpoints with a response of type `string` +- Endpoints with no arguments and a response of type `string` +- Unsupported endpoints: those have non-standard signatures +Deprecated functions are ignored. +*/ + +package main + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + "text/template" + "unicode" +) + +const ( + inputFilePath = "../../../../mobile/status.go" + templateFilePath = "./endpoints_template.txt" + outputFilePath = "../endpoints.go" +) + +var ( + // Regular expressions extracted as global variables + publicFuncWithArgsPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\((\w|\s)+\)\s+string\s+\{$`) + publicFuncWithoutArgsPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\(\)\s+string\s+\{$`) + funcNamePattern = regexp.MustCompile(`^func\s+(\w*)\(`) + deprecatedRegex = regexp.MustCompile(`(?i)//\s*Deprecated`) +) + +type TemplateData struct { + PackageName string + FunctionsWithResp []string + FunctionsNoArgs []string + UnsupportedEndpoints []string + DeprecatedEndpoints []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 publicFunctionsWithArgs []string + var publicFunctionsWithoutArgs []string + var unsupportedFunctions []string + var deprecatedFucntions []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 + } + + functionName := extractFunctionName(line) + if functionName == "" { + continue + } + + if !isPublicFunc(functionName) { + isDeprecated = false + continue + } + + if isDeprecated { + isDeprecated = false + deprecatedFucntions = append(deprecatedFucntions, functionName) + } + + switch { + case isPublicFunctionWithArgs(line): + publicFunctionsWithArgs = append(publicFunctionsWithArgs, functionName) + case isPublicFunctionWithoutArgs(line): + publicFunctionsWithoutArgs = append(publicFunctionsWithoutArgs, functionName) + 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: publicFunctionsWithArgs, + FunctionsNoArgs: publicFunctionsWithoutArgs, + UnsupportedEndpoints: unsupportedFunctions, + DeprecatedEndpoints: deprecatedFucntions, + } + + // 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 isPublicFunctionWithArgs(line string) bool { + return publicFuncWithArgsPattern.MatchString(line) +} + +// Function to check if a line contains a public function with not arguments and a response of string +func isPublicFunctionWithoutArgs(line string) bool { + return publicFuncWithoutArgsPattern.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 deprecatedRegex.MatchString(line) +} + +func isPublicFunc(name string) bool { + return name != "" && unicode.IsUpper(rune(name[0])) +} diff --git a/cmd/statusd/server/signals_server.go b/cmd/statusd/server/signals_server.go index 661c58209..e2608659a 100644 --- a/cmd/statusd/server/signals_server.go +++ b/cmd/statusd/server/signals_server.go @@ -2,21 +2,26 @@ package server import ( "context" - "errors" + "io" "net" "net/http" + "strconv" "sync" "time" + "github.com/ethereum/go-ethereum/log" + "github.com/gorilla/websocket" - "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" "github.com/status-im/status-go/signal" ) type Server struct { server *http.Server + listener net.Listener + mux *http.ServeMux lock sync.Mutex connections map[*websocket.Conn]struct{} address string @@ -32,6 +37,14 @@ func (s *Server) Address() string { return s.address } +func (s *Server) Port() (int, error) { + _, portString, err := net.SplitHostPort(s.address) + if err != nil { + return 0, err + } + return strconv.Atoi(portString) +} + func (s *Server) Setup() { signal.SetMobileSignalHandler(s.signalHandler) } @@ -53,32 +66,37 @@ func (s *Server) Listen(address string) error { return errors.New("server already started") } + _, _, err := net.SplitHostPort(address) + if err != nil { + return errors.Wrap(err, "invalid address") + } + s.server = &http.Server{ Addr: address, 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) + 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 +133,63 @@ func (s *Server) signals(w http.ResponseWriter, r *http.Request) { s.connections[connection] = struct{}{} } + +func (s *Server) addEndpointWithResponse(name string, handler func(string) string) { + log.Debug("adding endpoint", "name", name) + s.mux.HandleFunc(name, 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)) + + s.setHeaders(name, w) + + _, err = w.Write([]byte(response)) + if err != nil { + log.Error("failed to write response: %w", err) + } + }) +} + +func (s *Server) addEndpointNoRequest(name string, handler func() string) { + log.Debug("adding endpoint", "name", name) + s.mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { + response := handler() + + s.setHeaders(name, w) + + _, err := w.Write([]byte(response)) + if err != nil { + log.Error("failed to write response: %w", err) + } + }) +} + +func (s *Server) addUnsupportedEndpoint(name string) { + log.Debug("marking unsupported endpoint", "name", name) + s.mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + }) +} + +func (s *Server) RegisterMobileAPI() { + for name, endpoint := range EndpointsWithRequest { + s.addEndpointWithResponse(name, endpoint) + } + for name, endpoint := range EndpointsWithoutRequest { + s.addEndpointNoRequest(name, endpoint) + } + for _, name := range EndpointsUnsupported { + s.addUnsupportedEndpoint(name) + } +} + +func (s *Server) setHeaders(name string, w http.ResponseWriter) { + if _, ok := EndpointsDeprecated[name]; ok { + w.Header().Set("Deprecation", "true") + } + w.Header().Set("Content-Type", "application/json") +} diff --git a/cmd/statusd/server/signals_server_test.go b/cmd/statusd/server/signals_server_test.go index fbc5ff784..c90665002 100644 --- a/cmd/statusd/server/signals_server_test.go +++ b/cmd/statusd/server/signals_server_test.go @@ -1,9 +1,12 @@ package server import ( + "bytes" "context" "encoding/json" "fmt" + "io" + "net/http" "net/url" "testing" "time" @@ -16,24 +19,37 @@ import ( "github.com/status-im/status-go/signal" ) -func TestSignalsServer(t *testing.T) { - server := NewServer() - server.Setup() - err := server.Listen("localhost:0") +func setupServer(t *testing.T) (*Server, string) { + srv := NewServer() + srv.Setup() + err := srv.Listen("localhost:0") require.NoError(t, err) - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - server.Stop(ctx) - }() - addr := server.Address() - serverURLString := fmt.Sprintf("ws://%s", addr) + addr := srv.Address() + + // Check URL + serverURLString := fmt.Sprintf("http://%s", addr) serverURL, err := url.Parse(serverURLString) require.NoError(t, err) + require.NotNil(t, serverURL) require.NotZero(t, serverURL.Port()) - connection, _, err := websocket.DefaultDialer.Dial(serverURLString+"/signals", nil) + return srv, addr +} + +func shutdownServer(srv *Server) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + srv.Stop(ctx) +} + +func TestSignals(t *testing.T) { + srv, serverURLString := setupServer(t) + go srv.Serve() + defer shutdownServer(srv) + + signalsURL := fmt.Sprintf("ws://%s/signals", serverURLString) + connection, _, err := websocket.DefaultDialer.Dial(signalsURL, nil) require.NoError(t, err) require.NotNil(t, connection) defer func() { @@ -68,6 +84,80 @@ func TestSignalsServer(t *testing.T) { require.Equal(t, sentEvent, receivedEvent) } +func TestMobileAPI(t *testing.T) { + // Setup fake endpoints + endpointsWithResponse := EndpointsWithRequest + endpointsNoRequest := EndpointsWithoutRequest + endpointsUnsupported := EndpointsUnsupported + t.Cleanup(func() { + EndpointsWithRequest = endpointsWithResponse + EndpointsWithoutRequest = endpointsNoRequest + EndpointsUnsupported = endpointsUnsupported + }) + + endpointWithResponse := "/" + randomAlphabeticalString(t, 5) + endpointNoRequest := "/" + randomAlphabeticalString(t, 5) + endpointUnsupported := "/" + randomAlphabeticalString(t, 5) + + request1 := randomAlphabeticalString(t, 5) + response1 := randomAlphabeticalString(t, 5) + response2 := randomAlphabeticalString(t, 5) + + EndpointsWithRequest = map[string]func(string) string{ + endpointWithResponse: func(request string) string { + require.Equal(t, request1, request) + return response1 + }, + } + EndpointsWithoutRequest = map[string]func() string{ + endpointNoRequest: func() string { + return response2 + }, + } + EndpointsUnsupported = []string{endpointUnsupported} + + // Setup server + srv, _ := setupServer(t) + defer shutdownServer(srv) + go srv.Serve() + srv.RegisterMobileAPI() + + requestBody := []byte(request1) + bodyReader := bytes.NewReader(requestBody) + + port, err := srv.Port() + require.NoError(t, err) + + serverURL := fmt.Sprintf("http://127.0.0.1:%d", port) + + // Test endpoints with response + resp, err := http.Post(serverURL+endpointWithResponse, "application/text", bodyReader) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, http.StatusOK, resp.StatusCode) + + responseBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, response1, string(responseBody)) + + // Test endpoints with no request + resp, err = http.Get(serverURL + endpointNoRequest) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, http.StatusOK, resp.StatusCode) + + responseBody, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, response2, string(responseBody)) + + // Test unsupported endpoint + resp, err = http.Get(serverURL + endpointUnsupported) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, http.StatusNotImplemented, resp.StatusCode) + +} + func randomAlphabeticalString(t *testing.T, n int) string { s, err := common.RandomAlphabeticalString(n) require.NoError(t, err) diff --git a/mobile/status.go b/mobile/status.go index 81b13588a..a121d85b4 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 callWithResponse(openAccounts, datadir) }