From 51c1193ed234fcb74275dd32bb96867a7779b7b6 Mon Sep 17 00:00:00 2001 From: frank Date: Fri, 13 Sep 2024 23:08:20 +0800 Subject: [PATCH] feat_: Implement request logging functionality (#5812) * feat_: Implement request logging functionality * chore_: lint fix * chore_: address review feedback * test_: check panic recovery panic * chore_: add json tags --- logutils/override.go | 14 +- logutils/requestlog/request_log.go | 59 +++ mobile/init_logging_test.go | 47 +++ mobile/status.go | 551 +++++++++++++++++++++++------ mobile/status_request_log.go | 88 +++++ mobile/status_request_log_test.go | 155 ++++++++ 6 files changed, 792 insertions(+), 122 deletions(-) create mode 100644 logutils/requestlog/request_log.go create mode 100644 mobile/init_logging_test.go create mode 100644 mobile/status_request_log.go create mode 100644 mobile/status_request_log_test.go diff --git a/logutils/override.go b/logutils/override.go index 771f95694..44498b2f4 100644 --- a/logutils/override.go +++ b/logutils/override.go @@ -10,13 +10,13 @@ import ( ) type LogSettings struct { - Enabled bool - MobileSystem bool - Level string - File string - MaxSize int - MaxBackups int - CompressRotated bool + Enabled bool `json:"Enabled"` + MobileSystem bool `json:"MobileSystem"` + Level string `json:"Level"` + File string `json:"File"` + MaxSize int `json:"MaxSize"` + MaxBackups int `json:"MaxBackups"` + CompressRotated bool `json:"CompressRotated"` } // OverrideWithStdLogger overwrites ethereum's root logger with a logger from golang std lib. diff --git a/logutils/requestlog/request_log.go b/logutils/requestlog/request_log.go new file mode 100644 index 000000000..0b48907c4 --- /dev/null +++ b/logutils/requestlog/request_log.go @@ -0,0 +1,59 @@ +package requestlog + +import ( + "errors" + "sync/atomic" + + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/logutils" +) + +var ( + // requestLogger is the request logger object + requestLogger log.Logger + // isRequestLoggingEnabled controls whether request logging is enabled + isRequestLoggingEnabled atomic.Bool +) + +// NewRequestLogger creates a new request logger object +func NewRequestLogger(ctx ...interface{}) log.Logger { + requestLogger = log.New(ctx...) + return requestLogger +} + +// EnableRequestLogging enables or disables RPC logging +func EnableRequestLogging(enable bool) { + if enable { + isRequestLoggingEnabled.Store(true) + } else { + isRequestLoggingEnabled.Store(false) + } +} + +// IsRequestLoggingEnabled returns whether RPC logging is enabled +func IsRequestLoggingEnabled() bool { + return isRequestLoggingEnabled.Load() +} + +// GetRequestLogger returns the RPC logger object +func GetRequestLogger() log.Logger { + return requestLogger +} + +func ConfigureAndEnableRequestLogging(file string) error { + log.Info("initialising request logger", "log file", file) + requestLogger := NewRequestLogger() + if file == "" { + return errors.New("log file path is required") + } + fileOpts := logutils.FileOptions{ + Filename: file, + MaxBackups: 1, + } + handler := logutils.FileHandlerWithRotation(fileOpts, log.LogfmtFormat()) + filteredHandler := log.LvlFilterHandler(log.LvlDebug, handler) + requestLogger.SetHandler(filteredHandler) + EnableRequestLogging(true) + return nil +} diff --git a/mobile/init_logging_test.go b/mobile/init_logging_test.go new file mode 100644 index 000000000..95b37bfe3 --- /dev/null +++ b/mobile/init_logging_test.go @@ -0,0 +1,47 @@ +package statusgo + +import ( + "fmt" + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/logutils/requestlog" + "github.com/status-im/status-go/protocol/requests" +) + +func TestInitLogging(t *testing.T) { + tempDir := t.TempDir() + t.Logf("temp dir: %s", tempDir) + gethLogFile := path.Join(tempDir, "geth.log") + requestsLogFile := path.Join(tempDir, "requests.log") + logSettings := fmt.Sprintf(`{"LogRequestGo": true, "LogRequestFile": "%s", "File": "%s", "Level": "INFO", "Enabled": true, "MobileSystem": false}`, requestsLogFile, gethLogFile) + response := InitLogging(logSettings) + require.Equal(t, `{"error":""}`, response) + _, err := os.Stat(gethLogFile) + require.NoError(t, err) + require.True(t, requestlog.IsRequestLoggingEnabled()) + + // requests log file should not be created yet + _, err = os.Stat(requestsLogFile) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) + + createAccountRequest := &requests.CreateAccount{ + DisplayName: "some-display-name", + CustomizationColor: "#ffffff", + Password: "some-password", + RootDataDir: tempDir, + LogFilePath: gethLogFile, + } + _, err = statusBackend.CreateAccountAndLogin(createAccountRequest) + require.NoError(t, err) + result := CallPrivateRPC(`{"jsonrpc":"2.0","method":"settings_getSettings","params":[],"id":1}`) + require.NotContains(t, result, "error") + // Check if request log file exists now + _, err = os.Stat(requestsLogFile) + require.NoError(t, err) + require.FileExists(t, requestsLogFile) +} diff --git a/mobile/status.go b/mobile/status.go index e77e991fb..6b22db8dc 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "os" "unsafe" validator "gopkg.in/go-playground/validator.v9" @@ -28,6 +27,7 @@ import ( "github.com/status-im/status-go/extkeys" "github.com/status-im/status-go/images" "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/logutils/requestlog" "github.com/status-im/status-go/multiaccounts" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/multiaccounts/settings" @@ -55,6 +55,10 @@ type InitializeApplicationResponse struct { } func InitializeApplication(requestJSON string) string { + return logAndCallString(initializeApplication, requestJSON) +} + +func initializeApplication(requestJSON string) string { var request requests.InitializeApplication err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { @@ -95,9 +99,13 @@ func InitializeApplication(requestJSON string) string { return string(data) } -// DEPRECATED: use InitializeApplication -// OpenAccounts opens database and returns accounts list. func OpenAccounts(datadir string) string { + return logAndCallString(openAccounts, datadir) +} + +// DEPRECATED: use InitializeApplication +// openAccounts opens database and returns accounts list. +func openAccounts(datadir string) string { statusBackend.UpdateRootDataDir(datadir) err := statusBackend.OpenAccounts() if err != nil { @@ -114,8 +122,12 @@ func OpenAccounts(datadir string) string { return string(data) } -// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature. func ExtractGroupMembershipSignatures(signaturePairsStr string) string { + return logAndCallString(extractGroupMembershipSignatures, signaturePairsStr) +} + +// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature. +func extractGroupMembershipSignatures(signaturePairsStr string) string { var signaturePairs [][2]string if err := json.Unmarshal([]byte(signaturePairsStr), &signaturePairs); err != nil { @@ -137,8 +149,12 @@ func ExtractGroupMembershipSignatures(signaturePairsStr string) string { return string(data) } -// SignGroupMembership signs a string containing group membership information. func SignGroupMembership(content string) string { + return logAndCallString(signGroupMembership, content) +} + +// signGroupMembership signs a string containing group membership information. +func signGroupMembership(content string) string { signature, err := statusBackend.SignGroupMembership(content) if err != nil { return makeJSONResponse(err) @@ -154,15 +170,18 @@ func SignGroupMembership(content string) string { return string(data) } -// GetNodeConfig returns the current config of the Status node func GetNodeConfig() string { + return logAndCallString(getNodeConfig) +} + +// getNodeConfig returns the current config of the Status node +func getNodeConfig() string { conf, err := statusBackend.GetNodeConfig() if err != nil { return makeJSONResponse(err) } respJSON, err := json.Marshal(conf) - if err != nil { return makeJSONResponse(err) } @@ -170,8 +189,12 @@ func GetNodeConfig() string { return string(respJSON) } -// ValidateNodeConfig validates config for the Status node. func ValidateNodeConfig(configJSON string) string { + return logAndCallString(validateNodeConfig, configJSON) +} + +// validateNodeConfig validates config for the Status node. +func validateNodeConfig(configJSON string) string { var resp APIDetailedResponse _, err := params.NewConfigFromJSON(configJSON) @@ -212,14 +235,22 @@ func ValidateNodeConfig(configJSON string) string { return string(respJSON) } -// ResetChainData removes chain data from data directory. func ResetChainData() string { + return logAndCallString(resetChainData) +} + +// resetChainData removes chain data from data directory. +func resetChainData() string { api.RunAsync(statusBackend.ResetChainData) return makeJSONResponse(nil) } -// CallRPC calls public APIs via RPC. func CallRPC(inputJSON string) string { + return logAndCallString(callRPC, inputJSON) +} + +// callRPC calls public APIs via RPC. +func callRPC(inputJSON string) string { resp, err := statusBackend.CallRPC(inputJSON) if err != nil { return makeJSONResponse(err) @@ -227,8 +258,12 @@ func CallRPC(inputJSON string) string { return resp } -// CallPrivateRPC calls both public and private APIs via RPC. func CallPrivateRPC(inputJSON string) string { + return logAndCallString(callPrivateRPC, inputJSON) +} + +// callPrivateRPC calls both public and private APIs via RPC. +func callPrivateRPC(inputJSON string) string { resp, err := statusBackend.CallPrivateRPC(inputJSON) if err != nil { return makeJSONResponse(err) @@ -236,18 +271,32 @@ func CallPrivateRPC(inputJSON string) string { return resp } -// VerifyAccountPassword verifies account password. func VerifyAccountPassword(keyStoreDir, address, password string) string { + return logAndCallString(verifyAccountPassword, keyStoreDir, address, password) +} + +// verifyAccountPassword verifies account password. +func verifyAccountPassword(keyStoreDir, address, password string) string { _, err := statusBackend.AccountManager().VerifyAccountPassword(keyStoreDir, address, password) return makeJSONResponse(err) } func VerifyDatabasePassword(keyUID, password string) string { - return makeJSONResponse(statusBackend.VerifyDatabasePassword(keyUID, password)) + return logAndCallString(verifyDatabasePassword, keyUID, password) +} + +// verifyDatabasePassword verifies database password. +func verifyDatabasePassword(keyUID, password string) string { + err := statusBackend.VerifyDatabasePassword(keyUID, password) + return makeJSONResponse(err) } -// MigrateKeyStoreDir migrates key files to a new directory func MigrateKeyStoreDir(accountData, password, oldDir, newDir string) string { + return logAndCallString(migrateKeyStoreDir, accountData, password, oldDir, newDir) +} + +// migrateKeyStoreDir migrates key files to a new directory +func migrateKeyStoreDir(accountData, password, oldDir, newDir string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) if err != nil { @@ -255,13 +304,10 @@ func MigrateKeyStoreDir(accountData, password, oldDir, newDir string) string { } err = statusBackend.MigrateKeyStoreDir(account, password, oldDir, newDir) - if err != nil { - return makeJSONResponse(err) - } - - return makeJSONResponse(nil) + return makeJSONResponse(err) } +// login deprecated as Login and LoginWithConfig are deprecated func login(accountData, password, configJSON string) error { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) @@ -300,6 +346,8 @@ func login(accountData, password, configJSON string) error { // Login loads a key file (for a given address), tries to decrypt it using the password, // to verify ownership if verified, purges all the previous identities from Whisper, // and injects verified key as shh identity. +// +// Deprecated: Use LoginAccount instead. func Login(accountData, password string) string { err := login(accountData, password, "") if err != nil { @@ -323,6 +371,10 @@ func LoginWithConfig(accountData, password, configJSON string) string { } func CreateAccountAndLogin(requestJSON string) string { + return logAndCallString(createAccountAndLogin, requestJSON) +} + +func createAccountAndLogin(requestJSON string) string { var request requests.CreateAccount err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { @@ -350,6 +402,10 @@ func CreateAccountAndLogin(requestJSON string) string { } func LoginAccount(requestJSON string) string { + return logAndCallString(loginAccount, requestJSON) +} + +func loginAccount(requestJSON string) string { var request requests.Login err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { @@ -374,6 +430,10 @@ func LoginAccount(requestJSON string) string { } func RestoreAccountAndLogin(requestJSON string) string { + return logAndCallString(restoreAccountAndLogin, requestJSON) +} + +func restoreAccountAndLogin(requestJSON string) string { var request requests.RestoreAccount err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { @@ -447,28 +507,32 @@ func SaveAccountAndLogin(accountData, password, settingsJSON, configJSON, subacc return makeJSONResponse(nil) } -// DeleteMultiaccount func DeleteMultiaccount(keyUID, keyStoreDir string) string { + return logAndCallString(deleteMultiaccount, keyUID, keyStoreDir) +} + +// deleteMultiaccount +func deleteMultiaccount(keyUID, keyStoreDir string) string { err := statusBackend.DeleteMultiaccount(keyUID, keyStoreDir) - if err != nil { - return makeJSONResponse(err) - } - - return makeJSONResponse(nil) + return makeJSONResponse(err) } -// DeleteImportedKey func DeleteImportedKey(address, password, keyStoreDir string) string { - err := statusBackend.DeleteImportedKey(address, password, keyStoreDir) - if err != nil { - return makeJSONResponse(err) - } - - return makeJSONResponse(nil) + return logAndCallString(deleteImportedKey, address, password, keyStoreDir) +} + +// deleteImportedKey +func deleteImportedKey(address, password, keyStoreDir string) string { + err := statusBackend.DeleteImportedKey(address, password, keyStoreDir) + return makeJSONResponse(err) } -// InitKeystore initialize keystore before doing any operations with keys. func InitKeystore(keydir string) string { + return logAndCallString(initKeystore, keydir) +} + +// initKeystore initialize keystore before doing any operations with keys. +func initKeystore(keydir string) string { err := statusBackend.AccountManager().InitKeystore(keydir) return makeJSONResponse(err) } @@ -537,15 +601,22 @@ func LoginWithKeycard(accountData, password, keyHex string, configJSON string) s return makeJSONResponse(nil) } -// Logout is equivalent to clearing whisper identities. func Logout() string { - err := statusBackend.Logout() - return makeJSONResponse(err) + return logAndCallString(logout) +} + +// logout is equivalent to clearing whisper identities. +func logout() string { + return makeJSONResponse(statusBackend.Logout()) } -// SignMessage unmarshals rpc params {data, address, password} and -// passes them onto backend.SignMessage. func SignMessage(rpcParams string) string { + return logAndCallString(signMessage, rpcParams) +} + +// signMessage unmarshals rpc params {data, address, password} and +// passes them onto backend.SignMessage. +func signMessage(rpcParams string) string { var params personal.SignParams err := json.Unmarshal([]byte(rpcParams), ¶ms) if err != nil { @@ -560,6 +631,10 @@ func SignMessage(rpcParams string) string { // //export SignTypedData func SignTypedData(data, address, password string) string { + return logAndCallString(signTypedData, data, address, password) +} + +func signTypedData(data, address, password string) string { var typed typeddata.TypedData err := json.Unmarshal([]byte(data), &typed) if err != nil { @@ -576,6 +651,10 @@ func SignTypedData(data, address, password string) string { // //export HashTypedData func HashTypedData(data string) string { + return logAndCallString(hashTypedData, data) +} + +func hashTypedData(data string) string { var typed typeddata.TypedData err := json.Unmarshal([]byte(data), &typed) if err != nil { @@ -593,6 +672,10 @@ func HashTypedData(data string) string { // //export SignTypedDataV4 func SignTypedDataV4(data, address, password string) string { + return logAndCallString(signTypedDataV4, data, address, password) +} + +func signTypedDataV4(data, address, password string) string { var typed apitypes.TypedData err := json.Unmarshal([]byte(data), &typed) if err != nil { @@ -606,6 +689,10 @@ func SignTypedDataV4(data, address, password string) string { // //export HashTypedDataV4 func HashTypedDataV4(data string) string { + return logAndCallString(hashTypedDataV4, data) +} + +func hashTypedDataV4(data string) string { var typed apitypes.TypedData err := json.Unmarshal([]byte(data), &typed) if err != nil { @@ -615,9 +702,13 @@ func HashTypedDataV4(data string) string { return prepareJSONResponse(result.String(), err) } -// Recover unmarshals rpc params {signDataString, signedData} and passes -// them onto backend. func Recover(rpcParams string) string { + return logAndCallString(recoverWithRPCParams, rpcParams) +} + +// recoverWithRPCParams unmarshals rpc params {signDataString, signedData} and passes +// them onto backend. +func recoverWithRPCParams(rpcParams string) string { var params personal.RecoverParams err := json.Unmarshal([]byte(rpcParams), ¶ms) if err != nil { @@ -627,8 +718,12 @@ func Recover(rpcParams string) string { return prepareJSONResponse(addr.String(), err) } -// SendTransactionWithChainID converts RPC args and calls backend.SendTransactionWithChainID. func SendTransactionWithChainID(chainID int, txArgsJSON, password string) string { + return logAndCallString(sendTransactionWithChainID, chainID, txArgsJSON, password) +} + +// sendTransactionWithChainID converts RPC args and calls backend.SendTransactionWithChainID. +func sendTransactionWithChainID(chainID int, txArgsJSON, password string) string { var params transactions.SendTxArgs err := json.Unmarshal([]byte(txArgsJSON), ¶ms) if err != nil { @@ -642,8 +737,12 @@ func SendTransactionWithChainID(chainID int, txArgsJSON, password string) string return prepareJSONResponseWithCode(hash.String(), err, code) } -// SendTransaction converts RPC args and calls backend.SendTransaction. func SendTransaction(txArgsJSON, password string) string { + return logAndCallString(sendTransaction, txArgsJSON, password) +} + +// sendTransaction converts RPC args and calls backend.SendTransaction. +func sendTransaction(txArgsJSON, password string) string { var params transactions.SendTxArgs err := json.Unmarshal([]byte(txArgsJSON), ¶ms) if err != nil { @@ -657,8 +756,12 @@ func SendTransaction(txArgsJSON, password string) string { return prepareJSONResponseWithCode(hash.String(), err, code) } -// SendTransactionWithSignature converts RPC args and calls backend.SendTransactionWithSignature func SendTransactionWithSignature(txArgsJSON, sigString string) string { + return logAndCallString(sendTransactionWithSignature, txArgsJSON, sigString) +} + +// sendTransactionWithSignature converts RPC args and calls backend.SendTransactionWithSignature +func sendTransactionWithSignature(txArgsJSON, sigString string) string { var params transactions.SendTxArgs err := json.Unmarshal([]byte(txArgsJSON), ¶ms) if err != nil { @@ -678,8 +781,12 @@ func SendTransactionWithSignature(txArgsJSON, sigString string) string { return prepareJSONResponseWithCode(hash.String(), err, code) } -// HashTransaction validate the transaction and returns new txArgs and the transaction hash. func HashTransaction(txArgsJSON string) string { + return logAndCallString(hashTransaction, txArgsJSON) +} + +// hashTransaction validate the transaction and returns new txArgs and the transaction hash. +func hashTransaction(txArgsJSON string) string { var params transactions.SendTxArgs err := json.Unmarshal([]byte(txArgsJSON), ¶ms) if err != nil { @@ -703,13 +810,17 @@ func HashTransaction(txArgsJSON string) string { return prepareJSONResponseWithCode(result, err, code) } -// HashMessage calculates the hash of a message to be safely signed by the keycard +func HashMessage(message string) string { + return logAndCallString(hashMessage, message) +} + +// hashMessage calculates the hash of a message to be safely signed by the keycard // The hash is calulcated as // // keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). // // This gives context to the signed message and prevents signing of transactions. -func HashMessage(message string) string { +func hashMessage(message string) string { hash, err := api.HashMessage(message) code := codeUnknown if c, ok := errToCodeMap[err]; ok { @@ -718,20 +829,32 @@ func HashMessage(message string) string { return prepareJSONResponseWithCode(fmt.Sprintf("0x%x", hash), err, code) } -// StartCPUProfile runs pprof for CPU. func StartCPUProfile(dataDir string) string { + return logAndCallString(startCPUProfile, dataDir) +} + +// startCPUProfile runs pprof for CPU. +func startCPUProfile(dataDir string) string { err := profiling.StartCPUProfile(dataDir) return makeJSONResponse(err) } -// StopCPUProfiling stops pprof for cpu. -func StopCPUProfiling() string { //nolint: deadcode +func StopCPUProfiling() string { + return logAndCallString(stopCPUProfiling) +} + +// stopCPUProfiling stops pprof for cpu. +func stopCPUProfiling() string { //nolint: deadcode err := profiling.StopCPUProfile() return makeJSONResponse(err) } -// WriteHeapProfile starts pprof for heap -func WriteHeapProfile(dataDir string) string { //nolint: deadcode +func WriteHeapProfile(dataDir string) string { + return logAndCallString(writeHeapProfile, dataDir) +} + +// writeHeapProfile starts pprof for heap +func writeHeapProfile(dataDir string) string { //nolint: deadcode err := profiling.WriteHeapFile(dataDir) return makeJSONResponse(err) } @@ -739,7 +862,7 @@ func WriteHeapProfile(dataDir string) string { //nolint: deadcode func makeJSONResponse(err error) string { errString := "" if err != nil { - fmt.Fprintln(os.Stderr, err) + log.Error("error in makeJSONResponse", "error", err) errString = err.Error() } @@ -751,38 +874,62 @@ func makeJSONResponse(err error) string { return string(outBytes) } -// AddPeer adds an enode as a peer. func AddPeer(enode string) string { + return logAndCallString(addPeer, enode) +} + +// addPeer adds an enode as a peer. +func addPeer(enode string) string { err := statusBackend.StatusNode().AddPeer(enode) return makeJSONResponse(err) } -// ConnectionChange handles network state changes as reported -// by ReactNative (see https://facebook.github.io/react-native/docs/netinfo.html) func ConnectionChange(typ string, expensive int) { + logAndCall(connectionChange, typ, expensive) +} + +// connectionChange handles network state changes as reported +// by ReactNative (see https://facebook.github.io/react-native/docs/netinfo.html) +func connectionChange(typ string, expensive int) { statusBackend.ConnectionChange(typ, expensive == 1) } -// AppStateChange handles app state changes (background/foreground). func AppStateChange(state string) { + logAndCall(appStateChange, state) +} + +// appStateChange handles app state changes (background/foreground). +func appStateChange(state string) { statusBackend.AppStateChange(state) } -// StartLocalNotifications func StartLocalNotifications() string { + return logAndCallString(startLocalNotifications) +} + +// startLocalNotifications +func startLocalNotifications() string { err := statusBackend.StartLocalNotifications() return makeJSONResponse(err) } -// StopLocalNotifications func StopLocalNotifications() string { + return logAndCallString(stopLocalNotifications) +} + +// stopLocalNotifications +func stopLocalNotifications() string { err := statusBackend.StopLocalNotifications() return makeJSONResponse(err) } -// SetMobileSignalHandler setup geth callback to notify about new signal -// used for gomobile builds func SetMobileSignalHandler(handler SignalHandler) { + logAndCall(setMobileSignalHandler, handler) +} + +// setMobileSignalHandler setup geth callback to notify about new signal +// used for gomobile builds +func setMobileSignalHandler(handler SignalHandler) { signal.SetMobileSignalHandler(func(data []byte) { if len(data) > 0 { handler.HandleSignal(string(data)) @@ -790,8 +937,12 @@ func SetMobileSignalHandler(handler SignalHandler) { }) } -// SetSignalEventCallback setup geth callback to notify about new signal func SetSignalEventCallback(cb unsafe.Pointer) { + logAndCall(setSignalEventCallback, cb) +} + +// setSignalEventCallback setup geth callback to notify about new signal +func setSignalEventCallback(cb unsafe.Pointer) { signal.SetSignalEventCallback(cb) } @@ -799,6 +950,10 @@ func SetSignalEventCallback(cb unsafe.Pointer) { // //export ExportNodeLogs func ExportNodeLogs() string { + return logAndCallString(exportNodeLogs) +} + +func exportNodeLogs() string { node := statusBackend.StatusNode() if node == nil { return makeJSONResponse(errors.New("node is not running")) @@ -814,45 +969,76 @@ func ExportNodeLogs() string { return string(data) } -// SignHash exposes vanilla ECDSA signing required for Swarm messages func SignHash(hexEncodedHash string) string { + return logAndCallString(signHash, hexEncodedHash) +} + +// signHash exposes vanilla ECDSA signing required for Swarm messages +func signHash(hexEncodedHash string) string { hexEncodedSignature, err := statusBackend.SignHash(hexEncodedHash) if err != nil { return makeJSONResponse(err) } - return hexEncodedSignature } func GenerateAlias(pk string) string { + return logAndCallString(generateAlias, pk) +} + +func generateAlias(pk string) string { // We ignore any error, empty string is considered an error name, _ := protocol.GenerateAlias(pk) return name } func IsAlias(value string) string { + return logAndCallString(isAlias, value) +} + +func isAlias(value string) string { return prepareJSONResponse(alias.IsAlias(value), nil) } func Identicon(pk string) string { + return logAndCallString(identicon, pk) +} + +func identicon(pk string) string { // We ignore any error, empty string is considered an error identicon, _ := protocol.Identicon(pk) return identicon } func EmojiHash(pk string) string { + return logAndCallString(emojiHash, pk) +} + +func emojiHash(pk string) string { return prepareJSONResponse(emojihash.GenerateFor(pk)) } func ColorHash(pk string) string { + return logAndCallString(colorHash, pk) +} + +func colorHash(pk string) string { return prepareJSONResponse(colorhash.GenerateFor(pk)) } func ColorID(pk string) string { + return logAndCallString(colorID, pk) +} + +func colorID(pk string) string { return prepareJSONResponse(identityUtils.ToColorID(pk)) } func ValidateMnemonic(mnemonic string) string { + return logAndCallString(validateMnemonic, mnemonic) +} + +func validateMnemonic(mnemonic string) string { m := extkeys.NewMnemonic() err := m.ValidateMnemonic(mnemonic, extkeys.Language(0)) if err != nil { @@ -860,7 +1046,6 @@ func ValidateMnemonic(mnemonic string) string { } keyUID, err := statusBackend.GetKeyUIDByMnemonic(mnemonic) - if err != nil { return makeJSONResponse(err) } @@ -873,8 +1058,12 @@ func ValidateMnemonic(mnemonic string) string { return string(data) } -// DecompressPublicKey decompresses 33-byte compressed format to uncompressed 65-byte format. func DecompressPublicKey(key string) string { + return logAndCallString(decompressPublicKey, key) +} + +// decompressPublicKey decompresses 33-byte compressed format to uncompressed 65-byte format. +func decompressPublicKey(key string) string { decoded, err := types.DecodeHex(key) if err != nil { return makeJSONResponse(err) @@ -890,8 +1079,12 @@ func DecompressPublicKey(key string) string { return types.EncodeHex(crypto.FromECDSAPub(pubKey)) } -// CompressPublicKey compresses uncompressed 65-byte format to 33-byte compressed format. func CompressPublicKey(key string) string { + return logAndCallString(compressPublicKey, key) +} + +// compressPublicKey compresses uncompressed 65-byte format to 33-byte compressed format. +func compressPublicKey(key string) string { pubKey, err := common.HexToPubkey(key) if err != nil { return makeJSONResponse(err) @@ -899,75 +1092,93 @@ func CompressPublicKey(key string) string { return types.EncodeHex(crypto.CompressPubkey(pubKey)) } -// SerializeLegacyKey compresses an old format public key (0x04...) to the new one zQ... func SerializeLegacyKey(key string) string { + return logAndCallString(serializeLegacyKey, key) +} + +// serializeLegacyKey compresses an old format public key (0x04...) to the new one zQ... +func serializeLegacyKey(key string) string { cpk, err := multiformat.SerializeLegacyKey(key) if err != nil { return makeJSONResponse(err) } - return cpk } +func MultiformatSerializePublicKey(key, outBase string) string { + return logAndCallString(multiformatSerializePublicKey, key, outBase) +} + // SerializePublicKey compresses an uncompressed multibase encoded multicodec identified EC public key // For details on usage see specs https://specs.status.im/spec/2#public-key-serialization -func MultiformatSerializePublicKey(key, outBase string) string { +func multiformatSerializePublicKey(key, outBase string) string { cpk, err := multiformat.SerializePublicKey(key, outBase) if err != nil { return makeJSONResponse(err) } - return cpk } +func MultiformatDeserializePublicKey(key, outBase string) string { + return logAndCallString(multiformatDeserializePublicKey, key, outBase) +} + // DeserializePublicKey decompresses a compressed multibase encoded multicodec identified EC public key // For details on usage see specs https://specs.status.im/spec/2#public-key-serialization -func MultiformatDeserializePublicKey(key, outBase string) string { +func multiformatDeserializePublicKey(key, outBase string) string { pk, err := multiformat.DeserializePublicKey(key, outBase) if err != nil { return makeJSONResponse(err) } - return pk } -// ExportUnencryptedDatabase exports the database unencrypted to the given path func ExportUnencryptedDatabase(accountData, password, databasePath string) string { + return logAndCallString(exportUnencryptedDatabase, accountData, password, databasePath) +} + +// exportUnencryptedDatabase exports the database unencrypted to the given path +func exportUnencryptedDatabase(accountData, password, databasePath string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) if err != nil { return makeJSONResponse(err) } err = statusBackend.ExportUnencryptedDatabase(account, password, databasePath) - if err != nil { - return makeJSONResponse(err) - } - return makeJSONResponse(nil) + return makeJSONResponse(err) } -// ImportUnencryptedDatabase imports the database unencrypted to the given directory func ImportUnencryptedDatabase(accountData, password, databasePath string) string { + return logAndCallString(importUnencryptedDatabase, accountData, password, databasePath) +} + +// importUnencryptedDatabase imports the database unencrypted to the given directory +func importUnencryptedDatabase(accountData, password, databasePath string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) if err != nil { return makeJSONResponse(err) } err = statusBackend.ImportUnencryptedDatabase(account, password, databasePath) - if err != nil { - return makeJSONResponse(err) - } - return makeJSONResponse(nil) + return makeJSONResponse(err) } func ChangeDatabasePassword(KeyUID, password, newPassword string) string { + return logAndCallString(changeDatabasePassword, KeyUID, password, newPassword) +} + +// changeDatabasePassword changes the password of the database +func changeDatabasePassword(KeyUID, password, newPassword string) string { err := statusBackend.ChangeDatabasePassword(KeyUID, password, newPassword) - if err != nil { - return makeJSONResponse(err) - } - return makeJSONResponse(nil) + return makeJSONResponse(err) } func ConvertToKeycardAccount(accountData, settingsJSON, keycardUID, password, newPassword string) string { + return logAndCallString(convertToKeycardAccount, accountData, settingsJSON, keycardUID, password, newPassword) +} + +// convertToKeycardAccount converts the account to a keycard account +func convertToKeycardAccount(accountData, settingsJSON, keycardUID, password, newPassword string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) if err != nil { @@ -980,27 +1191,24 @@ func ConvertToKeycardAccount(accountData, settingsJSON, keycardUID, password, ne } err = statusBackend.ConvertToKeycardAccount(account, settings, keycardUID, password, newPassword) - if err != nil { - return makeJSONResponse(err) - } - return makeJSONResponse(nil) + return makeJSONResponse(err) } func ConvertToRegularAccount(mnemonic, currPassword, newPassword string) string { + return logAndCallString(convertToRegularAccount, mnemonic, currPassword, newPassword) +} + +// convertToRegularAccount converts the account to a regular account +func convertToRegularAccount(mnemonic, currPassword, newPassword string) string { err := statusBackend.ConvertToRegularAccount(mnemonic, currPassword, newPassword) - if err != nil { - return makeJSONResponse(err) - } - return makeJSONResponse(nil) + return makeJSONResponse(err) } func ImageServerTLSCert() string { cert, err := server.PublicMediaTLSCert() - if err != nil { return makeJSONResponse(err) } - return cert } @@ -1063,6 +1271,10 @@ type FleetDescription struct { } func Fleets() string { + return logAndCallString(fleets) +} + +func fleets() string { fleets := FleetDescription{ DefaultFleet: api.DefaultFleet, Fleets: params.GetSupportedFleets(), @@ -1076,6 +1288,10 @@ func Fleets() string { } func SwitchFleet(fleet string, configJSON string) string { + return logAndCallString(switchFleet, fleet, configJSON) +} + +func switchFleet(fleet string, configJSON string) string { var conf params.NodeConfig if configJSON != "" { err := json.Unmarshal([]byte(configJSON), &conf) @@ -1110,17 +1326,25 @@ func GenerateImages(filepath string, aX, aY, bX, bY int) string { return string(data) } -// LocalPairingPreflightOutboundCheck creates a local tls server accessible via an outbound network address. +func LocalPairingPreflightOutboundCheck() string { + return logAndCallString(localPairingPreflightOutboundCheck) +} + +// localPairingPreflightOutboundCheck creates a local tls server accessible via an outbound network address. // The function creates a client and makes an outbound network call to the local server. This function should be // triggered to ensure that the device has permissions to access its LAN or to make outbound network calls. // // In addition, the functionality attempts to address an issue with iOS devices https://stackoverflow.com/a/64242745 -func LocalPairingPreflightOutboundCheck() string { +func localPairingPreflightOutboundCheck() string { err := preflight.CheckOutbound() return makeJSONResponse(err) } -// StartSearchForLocalPairingPeers starts a UDP multicast beacon that both listens for and broadcasts to LAN peers +func StartSearchForLocalPairingPeers() string { + return logAndCallString(startSearchForLocalPairingPeers) +} + +// startSearchForLocalPairingPeers starts a UDP multicast beacon that both listens for and broadcasts to LAN peers // on discovery the beacon will emit a signal with the details of the discovered peer. // // Currently, beacons are configured to search for 2 minutes pinging the network every 500 ms; @@ -1129,18 +1353,22 @@ func LocalPairingPreflightOutboundCheck() string { // reasonable time to discover this device. // // Peer details are represented by a json.Marshal peers.LocalPairingPeerHello -func StartSearchForLocalPairingPeers() string { +func startSearchForLocalPairingPeers() string { pn := pairing.NewPeerNotifier() err := pn.Search() return makeJSONResponse(err) } -// GetConnectionStringForBeingBootstrapped starts a pairing.ReceiverServer +func GetConnectionStringForBeingBootstrapped(configJSON string) string { + return logAndCallString(getConnectionStringForBeingBootstrapped, configJSON) +} + +// getConnectionStringForBeingBootstrapped starts a pairing.ReceiverServer // then generates a pairing.ConnectionParams. Used when the device is Logged out or has no Account keys // and the device has no camera to read a QR code with // // Example: A desktop device (device without camera) receiving account data from mobile (device with camera) -func GetConnectionStringForBeingBootstrapped(configJSON string) string { +func getConnectionStringForBeingBootstrapped(configJSON string) string { if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, PayloadSourceConfig is expected")) } @@ -1163,13 +1391,17 @@ func GetConnectionStringForBeingBootstrapped(configJSON string) string { return cs } -// GetConnectionStringForBootstrappingAnotherDevice starts a pairing.SenderServer +func GetConnectionStringForBootstrappingAnotherDevice(configJSON string) string { + return logAndCallString(getConnectionStringForBootstrappingAnotherDevice, configJSON) +} + +// getConnectionStringForBootstrappingAnotherDevice starts a pairing.SenderServer // then generates a pairing.ConnectionParams. Used when the device is Logged in and therefore has Account keys // and the device might not have a camera // // Example: A mobile or desktop device (devices that MAY have a camera but MUST have a screen) // sending account data to a mobile (device with camera) -func GetConnectionStringForBootstrappingAnotherDevice(configJSON string) string { +func getConnectionStringForBootstrappingAnotherDevice(configJSON string) string { if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, SendingServerConfig is expected")) } @@ -1198,7 +1430,11 @@ func (i *inputConnectionStringForBootstrappingResponse) toJSON(err error) string return string(j) } -// InputConnectionStringForBootstrapping starts a pairing.ReceiverClient +func InputConnectionStringForBootstrapping(cs, configJSON string) string { + return logAndCallString(inputConnectionStringForBootstrapping, cs, configJSON) +} + +// inputConnectionStringForBootstrapping starts a pairing.ReceiverClient // The given server.ConnectionParams string will determine the server.Mode // // server.Mode = server.Sending @@ -1206,7 +1442,7 @@ func (i *inputConnectionStringForBootstrappingResponse) toJSON(err error) string // // Example: A mobile device (device with a camera) receiving account data from // a device with a screen (mobile or desktop devices) -func InputConnectionStringForBootstrapping(cs, configJSON string) string { +func inputConnectionStringForBootstrapping(cs, configJSON string) string { var err error if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, ReceiverClientConfig is expected")) @@ -1232,20 +1468,23 @@ func InputConnectionStringForBootstrapping(cs, configJSON string) string { err = pairing.StartUpReceivingClient(statusBackend, cs, configJSON) if err != nil { return response.toJSON(err) - } return response.toJSON(statusBackend.Logout()) } -// InputConnectionStringForBootstrappingAnotherDevice starts a pairing.SendingClient +func InputConnectionStringForBootstrappingAnotherDevice(cs, configJSON string) string { + return logAndCallString(inputConnectionStringForBootstrappingAnotherDevice, cs, configJSON) +} + +// inputConnectionStringForBootstrappingAnotherDevice starts a pairing.SendingClient // The given server.ConnectionParams string will determine the server.Mode // // server.Mode = server.Receiving // Used when the device is Logged in and therefore has Account keys and the has a camera to read a QR code // // Example: A mobile (device with camera) sending account data to a desktop device (device without camera) -func InputConnectionStringForBootstrappingAnotherDevice(cs, configJSON string) string { +func inputConnectionStringForBootstrappingAnotherDevice(cs, configJSON string) string { var err error if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, SenderClientConfig is expected")) @@ -1261,10 +1500,14 @@ func InputConnectionStringForBootstrappingAnotherDevice(cs, configJSON string) s return makeJSONResponse(err) } -// GetConnectionStringForExportingKeypairsKeystores starts a pairing.SenderServer +func GetConnectionStringForExportingKeypairsKeystores(configJSON string) string { + return logAndCallString(getConnectionStringForExportingKeypairsKeystores, configJSON) +} + +// getConnectionStringForExportingKeypairsKeystores starts a pairing.SenderServer // then generates a pairing.ConnectionParams. Used when the device is Logged in and therefore has Account keys // and the device might not have a camera, to transfer kestore files of provided key uids. -func GetConnectionStringForExportingKeypairsKeystores(configJSON string) string { +func getConnectionStringForExportingKeypairsKeystores(configJSON string) string { if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, SendingServerConfig is expected")) } @@ -1276,13 +1519,17 @@ func GetConnectionStringForExportingKeypairsKeystores(configJSON string) string return cs } -// InputConnectionStringForImportingKeypairsKeystores starts a pairing.ReceiverClient +func InputConnectionStringForImportingKeypairsKeystores(cs, configJSON string) string { + return logAndCallString(inputConnectionStringForImportingKeypairsKeystores, cs, configJSON) +} + +// inputConnectionStringForImportingKeypairsKeystores starts a pairing.ReceiverClient // The given server.ConnectionParams string will determine the server.Mode // Used when the device is Logged in and has Account keys and has a camera to read a QR code // // Example: A mobile device (device with a camera) receiving account data from // a device with a screen (mobile or desktop devices) -func InputConnectionStringForImportingKeypairsKeystores(cs, configJSON string) string { +func inputConnectionStringForImportingKeypairsKeystores(cs, configJSON string) string { if configJSON == "" { return makeJSONResponse(fmt.Errorf("no config given, ReceiverClientConfig is expected")) } @@ -1292,6 +1539,10 @@ func InputConnectionStringForImportingKeypairsKeystores(cs, configJSON string) s } func ValidateConnectionString(cs string) string { + return logAndCallString(validateConnectionString, cs) +} + +func validateConnectionString(cs string) string { err := pairing.ValidateConnectionString(cs) if err == nil { return "" @@ -1300,6 +1551,10 @@ func ValidateConnectionString(cs string) string { } func EncodeTransfer(to string, value string) string { + return logAndCallString(encodeTransfer, to, value) +} + +func encodeTransfer(to string, value string) string { result, err := abi_spec.EncodeTransfer(to, value) if err != nil { log.Error("failed to encode transfer", "to", to, "value", value, "error", err) @@ -1309,14 +1564,23 @@ func EncodeTransfer(to string, value string) string { } func EncodeFunctionCall(method string, paramsJSON string) string { + return logAndCallString(encodeFunctionCall, method, paramsJSON) +} + +func encodeFunctionCall(method string, paramsJSON string) string { result, err := abi_spec.Encode(method, paramsJSON) if err != nil { log.Error("failed to encode function call", "method", method, "paramsJSON", paramsJSON, "error", err) + return "" } return result } func DecodeParameters(decodeParamJSON string) string { + return decodeParameters(decodeParamJSON) +} + +func decodeParameters(decodeParamJSON string) string { decodeParam := struct { BytesString string `json:"bytesString"` Types []string `json:"types"` @@ -1340,10 +1604,18 @@ func DecodeParameters(decodeParamJSON string) string { } func HexToNumber(hex string) string { + return logAndCallString(hexToNumber, hex) +} + +func hexToNumber(hex string) string { return abi_spec.HexToNumber(hex) } func NumberToHex(numString string) string { + return logAndCallString(numberToHex, numString) +} + +func numberToHex(numString string) string { return abi_spec.NumberToHex(numString) } @@ -1352,6 +1624,10 @@ func Sha3(str string) string { } func Utf8ToHex(str string) string { + return logAndCallString(utf8ToHex, str) +} + +func utf8ToHex(str string) string { hexString, err := abi_spec.Utf8ToHex(str) if err != nil { log.Error("failed to convert utf8 to hex", "str", str, "error", err) @@ -1360,6 +1636,10 @@ func Utf8ToHex(str string) string { } func HexToUtf8(hexString string) string { + return logAndCallString(hexToUtf8, hexString) +} + +func hexToUtf8(hexString string) string { str, err := abi_spec.HexToUtf8(hexString) if err != nil { log.Error("failed to convert hex to utf8", "hexString", hexString, "error", err) @@ -1368,6 +1648,10 @@ func HexToUtf8(hexString string) string { } func CheckAddressChecksum(address string) string { + return logAndCallString(checkAddressChecksum, address) +} + +func checkAddressChecksum(address string) string { valid, err := abi_spec.CheckAddressChecksum(address) if err != nil { log.Error("failed to invoke check address checksum", "address", address, "error", err) @@ -1377,6 +1661,10 @@ func CheckAddressChecksum(address string) string { } func IsAddress(address string) string { + return logAndCallString(isAddress, address) +} + +func isAddress(address string) string { valid, err := abi_spec.IsAddress(address) if err != nil { log.Error("failed to invoke IsAddress", "address", address, "error", err) @@ -1386,6 +1674,10 @@ func IsAddress(address string) string { } func ToChecksumAddress(address string) string { + return logAndCallString(toChecksumAddress, address) +} + +func toChecksumAddress(address string) string { address, err := abi_spec.ToChecksumAddress(address) if err != nil { log.Error("failed to convert to checksum address", "address", address, "error", err) @@ -1394,25 +1686,42 @@ func ToChecksumAddress(address string) string { } func DeserializeAndCompressKey(DesktopKey string) string { + return logAndCallString(deserializeAndCompressKey, DesktopKey) +} + +func deserializeAndCompressKey(DesktopKey string) string { deserialisedKey := MultiformatDeserializePublicKey(DesktopKey, "f") sanitisedKey := "0x" + deserialisedKey[5:] return CompressPublicKey(sanitisedKey) } +type InitLoggingRequest struct { + logutils.LogSettings + LogRequestGo bool `json:"LogRequestGo"` + LogRequestFile string `json:"LogRequestFile"` +} + // InitLogging The InitLogging function should be called when the application starts. // This ensures that we can capture logs before the user login. Subsequent calls will update the logger settings. // Before this, we can only capture logs after user login since we will only configure the logging after the login process. func InitLogging(logSettingsJSON string) string { - var logSettings logutils.LogSettings + var logSettings InitLoggingRequest var err error if err = json.Unmarshal([]byte(logSettingsJSON), &logSettings); err != nil { return makeJSONResponse(err) } - if err = logutils.OverrideRootLogWithConfig(logSettings, false); err == nil { + if err = logutils.OverrideRootLogWithConfig(logSettings.LogSettings, false); err == nil { log.Info("logging initialised", "logSettings", logSettingsJSON) } + if logSettings.LogRequestGo { + err = requestlog.ConfigureAndEnableRequestLogging(logSettings.LogRequestFile) + if err != nil { + return makeJSONResponse(err) + } + } + return makeJSONResponse(err) } @@ -1425,6 +1734,10 @@ func GetRandomMnemonic() string { } func ToggleCentralizedMetrics(requestJSON string) string { + return logAndCallString(toggleCentralizedMetrics, requestJSON) +} + +func toggleCentralizedMetrics(requestJSON string) string { var request requests.ToggleCentralizedMetrics err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { @@ -1445,6 +1758,10 @@ func ToggleCentralizedMetrics(requestJSON string) string { } func CentralizedMetricsInfo() string { + return logAndCallString(centralizedMetricsInfo) +} + +func centralizedMetricsInfo() string { metricsInfo, err := statusBackend.CentralizedMetricsInfo() if err != nil { return makeJSONResponse(err) @@ -1457,6 +1774,10 @@ func CentralizedMetricsInfo() string { } func AddCentralizedMetric(requestJSON string) string { + return logAndCallString(addCentralizedMetric, requestJSON) +} + +func addCentralizedMetric(requestJSON string) string { var request requests.AddCentralizedMetric err := json.Unmarshal([]byte(requestJSON), &request) if err != nil { diff --git a/mobile/status_request_log.go b/mobile/status_request_log.go new file mode 100644 index 000000000..e3ddd8ba2 --- /dev/null +++ b/mobile/status_request_log.go @@ -0,0 +1,88 @@ +package statusgo + +import ( + "fmt" + "reflect" + "regexp" + "runtime" + "runtime/debug" + "strings" + "time" + + "github.com/ethereum/go-ethereum/log" + "github.com/status-im/status-go/logutils/requestlog" +) + +var sensitiveRegex = regexp.MustCompile(`(?i)(".*?(password|mnemonic|openseaAPIKey|poktToken|alchemyArbitrumMainnetToken|raribleTestnetAPIKey|alchemyOptimismMainnetToken|statusProxyBlockchainUser|alchemyEthereumSepoliaToken|alchemyArbitrumSepoliaToken|infuraToken|raribleMainnetAPIKey|alchemyEthereumMainnetToken).*?")\s*:\s*("[^"]*")`) + +func getFunctionName(fn any) string { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() +} + +func getShortFunctionName(fn any) string { + fullName := getFunctionName(fn) + parts := strings.Split(fullName, ".") + return parts[len(parts)-1] +} + +// logAndCall logs request call details and executes the fn function if logging is enabled +func logAndCall(fn any, params ...any) any { + defer func() { + if r := recover(); r != nil { + // we're not sure if request logging is enabled here, so we log it use default logger + log.Error("panic found in logAndCall", "error", r, "stacktrace", string(debug.Stack())) + panic(r) + } + }() + + var startTime time.Time + + if requestlog.IsRequestLoggingEnabled() { + startTime = time.Now() + } + + fnValue := reflect.ValueOf(fn) + fnType := fnValue.Type() + if fnType.Kind() != reflect.Func { + panic("fn must be a function") + } + + args := make([]reflect.Value, len(params)) + for i, param := range params { + args[i] = reflect.ValueOf(param) + } + + results := fnValue.Call(args) + + var resp any + + if len(results) > 0 { + resp = results[0].Interface() + } + + if requestlog.IsRequestLoggingEnabled() { + duration := time.Since(startTime) + methodName := getShortFunctionName(fn) + paramsString := removeSensitiveInfo(fmt.Sprintf("%+v", params)) + respString := removeSensitiveInfo(fmt.Sprintf("%+v", resp)) + requestlog.GetRequestLogger().Debug(methodName, "params", paramsString, "resp", respString, "duration", duration) + } + + return resp +} + +func logAndCallString(fn any, params ...any) string { + resp := logAndCall(fn, params...) + if resp == nil { + return "" + } + return resp.(string) +} + +func removeSensitiveInfo(jsonStr string) string { + // see related test for the usage of this function + return sensitiveRegex.ReplaceAllStringFunc(jsonStr, func(match string) string { + parts := sensitiveRegex.FindStringSubmatch(match) + return fmt.Sprintf(`%s:"***"`, parts[1]) + }) +} diff --git a/mobile/status_request_log_test.go b/mobile/status_request_log_test.go new file mode 100644 index 000000000..d1cdf267a --- /dev/null +++ b/mobile/status_request_log_test.go @@ -0,0 +1,155 @@ +package statusgo + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/logutils/requestlog" + "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/settings" + "github.com/status-im/status-go/signal" + + "github.com/ethereum/go-ethereum/log" +) + +func TestRemoveSensitiveInfo(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "basic test", + input: `{"username":"user1","password":"secret123","mnemonic":"mnemonic123 xyz"}`, + expected: `{"username":"user1","password":"***","mnemonic":"***"}`, + }, + { + name: "uppercase password field", + input: `{"USERNAME":"user1","PASSWORD":"secret123"}`, + expected: `{"USERNAME":"user1","PASSWORD":"***"}`, + }, + { + name: "password field with spaces", + input: `{"username":"user1", "password" : "secret123"}`, + expected: `{"username":"user1", "password":"***"}`, + }, + { + name: "multiple password fields", + input: `{"password":"secret123","data":{"nested_password":"nested_secret"}}`, + expected: `{"password":"***","data":{"nested_password":"***"}}`, + }, + { + name: "no password field", + input: `{"username":"user1","email":"user1@example.com"}`, + expected: `{"username":"user1","email":"user1@example.com"}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := removeSensitiveInfo(tc.input) + if result != tc.expected { + t.Errorf("Expected: %s, Got: %s", tc.expected, result) + } + }) + } +} + +func TestLogAndCall(t *testing.T) { + // Enable request logging + requestlog.EnableRequestLogging(true) + + // Create a mock logger to capture log output + var logOutput string + mockLogger := log.New() + mockLogger.SetHandler(log.FuncHandler(func(r *log.Record) error { + logOutput += r.Msg + fmt.Sprintf("%s", r.Ctx...) + return nil + })) + requestlog.NewRequestLogger().SetHandler(mockLogger.GetHandler()) + + // Test case 1: Normal execution + testFunc := func(param string) string { + return "test result: " + param + } + testParam := "test input" + expectedResult := "test result: test input" + + result := logAndCallString(testFunc, testParam) + + // Check the result + if result != expectedResult { + t.Errorf("Expected result %s, got %s", expectedResult, result) + } + + // Check if the log contains expected information + expectedLogParts := []string{getShortFunctionName(testFunc), "params", testParam, "resp", expectedResult} + for _, part := range expectedLogParts { + if !strings.Contains(logOutput, part) { + t.Errorf("Log output doesn't contain expected part: %s", part) + } + } + + // Test case 2: Panic -> recovery -> re-panic + oldRootHandler := log.Root().GetHandler() + defer log.Root().SetHandler(oldRootHandler) + log.Root().SetHandler(mockLogger.GetHandler()) + // Clear log output for next test + logOutput = "" + e := "test panic" + panicFunc := func() { + panic(e) + } + + require.PanicsWithValue(t, e, func() { + logAndCall(panicFunc) + }) + + // Check if the panic was logged + if !strings.Contains(logOutput, "panic found in logAndCall") { + t.Errorf("Log output doesn't contain panic information") + } + if !strings.Contains(logOutput, e) { + t.Errorf("Log output doesn't contain panic message") + } + if !strings.Contains(logOutput, "stacktrace") { + t.Errorf("Log output doesn't contain stacktrace") + } +} + +func TestGetFunctionName(t *testing.T) { + fn := getShortFunctionName(initializeApplication) + require.Equal(t, "initializeApplication", fn) +} + +type testSignalHandler struct { + receivedSignal string +} + +func (t *testSignalHandler) HandleSignal(data string) { + t.receivedSignal = data +} + +func TestSetMobileSignalHandler(t *testing.T) { + // Setup + handler := &testSignalHandler{} + SetMobileSignalHandler(handler) + t.Cleanup(signal.ResetMobileSignalHandler) + + // Test data + testAccount := &multiaccounts.Account{Name: "test"} + testSettings := &settings.Settings{KeyUID: "0x1"} + testEnsUsernames := json.RawMessage(`{"test": "test"}`) + + // Action + signal.SendLoggedIn(testAccount, testSettings, testEnsUsernames, nil) + + // Assertions + require.Contains(t, handler.receivedSignal, `"key-uid":"0x1"`, "Signal should contain the correct KeyUID") + require.Contains(t, handler.receivedSignal, `"name":"test"`, "Signal should contain the correct account name") + require.Contains(t, handler.receivedSignal, `"ensUsernames":{"test":"test"}`, "Signal should contain the correct ENS usernames") +}