feat(cmd)_: `status-backend` (#5847)
* feat_: server supports for mobile api fix(statusd)_: manually serve * feat_: generate endpoints list from status/mobile.go * chore_: mark public endpoint OpenAccounts as deprecated * chore_: added status-backend makefile targets * feat_: expose deprecated endpoints, with special header field * docs(status-backend)_: added README
This commit is contained in:
parent
42f715f123
commit
fc36a7e980
7
Makefile
7
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..."
|
||||
|
|
|
@ -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`
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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": {},
|
||||
}
|
|
@ -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 }}
|
||||
}
|
|
@ -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]))
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue