@onekeyfe/react-native-native-logger 1.1.20 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +1 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +8 -1
- package/android/src/main/java/com/margelo/nitro/nativelogger/NativeLogger.kt +64 -9
- package/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLog.kt +85 -11
- package/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLogInitProvider.kt +27 -0
- package/ios/NativeLogger.swift +70 -9
- package/ios/OneKeyLog.swift +84 -30
- package/lib/typescript/src/NativeLogger.nitro.d.ts +1 -0
- package/lib/typescript/src/NativeLogger.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNativeLoggerSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JHybridNativeLoggerSpec.hpp +1 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelogger/HybridNativeLoggerSpec.kt +4 -0
- package/nitrogen/generated/ios/ReactNativeNativeLogger-Swift-Cxx-Bridge.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridNativeLoggerSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/swift/HybridNativeLoggerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNativeLoggerSpec_cxx.swift +12 -0
- package/nitrogen/generated/shared/c++/HybridNativeLoggerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNativeLoggerSpec.hpp +1 -0
- package/package.json +1 -1
- package/src/NativeLogger.nitro.ts +1 -0
package/android/build.gradle
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<application>
|
|
3
|
+
<provider
|
|
4
|
+
android:name=".OneKeyLogInitProvider"
|
|
5
|
+
android:authorities="${applicationId}.onekeylog-init"
|
|
6
|
+
android:exported="false" />
|
|
7
|
+
</application>
|
|
8
|
+
</manifest>
|
|
@@ -7,27 +7,82 @@ import java.io.File
|
|
|
7
7
|
@DoNotStrip
|
|
8
8
|
class NativeLogger : HybridNativeLoggerSpec() {
|
|
9
9
|
|
|
10
|
+
companion object {
|
|
11
|
+
/** Patterns that should never be written to log files */
|
|
12
|
+
private val sensitivePatterns = listOf(
|
|
13
|
+
// Hex-encoded private keys (64 hex chars), with optional 0x prefix
|
|
14
|
+
Regex("(?:0x)?[0-9a-fA-F]{64}"),
|
|
15
|
+
// WIF private keys (base58, starting with 5, K, or L)
|
|
16
|
+
Regex("\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b"),
|
|
17
|
+
// Extended keys (xprv/xpub/zprv/zpub/yprv/ypub)
|
|
18
|
+
Regex("\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b"),
|
|
19
|
+
// BIP39 mnemonic-like sequences (12+ words of 3-8 lowercase letters)
|
|
20
|
+
Regex("(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b"),
|
|
21
|
+
// Bearer/API tokens
|
|
22
|
+
Regex("(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}"),
|
|
23
|
+
// Base64 encoded data that looks like keys (44+ chars)
|
|
24
|
+
Regex("(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
/** Rate limiting: max messages per second */
|
|
28
|
+
private const val MAX_MESSAGES_PER_SECOND = 100
|
|
29
|
+
@Volatile private var messageCount = 0
|
|
30
|
+
@Volatile private var windowStartMs = System.currentTimeMillis()
|
|
31
|
+
private val rateLimitLock = Any()
|
|
32
|
+
|
|
33
|
+
private fun isRateLimited(): Boolean {
|
|
34
|
+
synchronized(rateLimitLock) {
|
|
35
|
+
val now = System.currentTimeMillis()
|
|
36
|
+
if (now - windowStartMs >= 1000L) {
|
|
37
|
+
windowStartMs = now
|
|
38
|
+
messageCount = 0
|
|
39
|
+
}
|
|
40
|
+
messageCount++
|
|
41
|
+
return messageCount > MAX_MESSAGES_PER_SECOND
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun sanitize(message: String): String {
|
|
46
|
+
var result = message
|
|
47
|
+
for (pattern in sensitivePatterns) {
|
|
48
|
+
result = pattern.replace(result, "[REDACTED]")
|
|
49
|
+
}
|
|
50
|
+
// Strip newlines to prevent log injection
|
|
51
|
+
result = result.replace("\n", " ").replace("\r", " ")
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
10
56
|
override fun write(level: Double, msg: String) {
|
|
57
|
+
if (isRateLimited()) return
|
|
58
|
+
val sanitized = sanitize(msg)
|
|
11
59
|
when (level.toInt()) {
|
|
12
|
-
0 -> OneKeyLog.debug("JS",
|
|
13
|
-
1 -> OneKeyLog.info("JS",
|
|
14
|
-
2 -> OneKeyLog.warn("JS",
|
|
15
|
-
3 -> OneKeyLog.error("JS",
|
|
16
|
-
else -> OneKeyLog.info("JS",
|
|
60
|
+
0 -> OneKeyLog.debug("JS", sanitized)
|
|
61
|
+
1 -> OneKeyLog.info("JS", sanitized)
|
|
62
|
+
2 -> OneKeyLog.warn("JS", sanitized)
|
|
63
|
+
3 -> OneKeyLog.error("JS", sanitized)
|
|
64
|
+
else -> OneKeyLog.info("JS", sanitized)
|
|
17
65
|
}
|
|
18
66
|
}
|
|
19
67
|
|
|
68
|
+
override fun getLogDirectory(): String {
|
|
69
|
+
return OneKeyLog.logsDirectory
|
|
70
|
+
}
|
|
71
|
+
|
|
20
72
|
override fun getLogFilePaths(): Promise<Array<String>> {
|
|
21
73
|
return Promise.async {
|
|
74
|
+
// Flush buffered log data so the active file reflects all written logs
|
|
75
|
+
OneKeyLog.flush()
|
|
22
76
|
val dir = OneKeyLog.logsDirectory
|
|
23
77
|
if (dir.isEmpty()) return@async arrayOf<String>()
|
|
24
78
|
val files = File(dir).listFiles { _, name -> name.endsWith(".log") }
|
|
25
79
|
if (files == null) {
|
|
26
|
-
OneKeyLog.warn("NativeLogger", "Failed to list log directory
|
|
80
|
+
OneKeyLog.warn("NativeLogger", "Failed to list log directory")
|
|
27
81
|
return@async arrayOf<String>()
|
|
28
82
|
}
|
|
83
|
+
// Return filenames only, not absolute paths
|
|
29
84
|
files.sortedBy { it.name }
|
|
30
|
-
.map { it.
|
|
85
|
+
.map { it.name }.toTypedArray()
|
|
31
86
|
}
|
|
32
87
|
}
|
|
33
88
|
|
|
@@ -37,13 +92,13 @@ class NativeLogger : HybridNativeLoggerSpec() {
|
|
|
37
92
|
if (dir.isEmpty()) return@async
|
|
38
93
|
val files = File(dir).listFiles { _, name -> name.endsWith(".log") }
|
|
39
94
|
if (files == null) {
|
|
40
|
-
OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion
|
|
95
|
+
OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion")
|
|
41
96
|
return@async
|
|
42
97
|
}
|
|
43
98
|
// Skip the active log file to avoid breaking logback's open file handle
|
|
44
99
|
files.filter { it.name != "app-latest.log" }.forEach { file ->
|
|
45
100
|
if (!file.delete()) {
|
|
46
|
-
OneKeyLog.warn("NativeLogger", "Failed to delete log file
|
|
101
|
+
OneKeyLog.warn("NativeLogger", "Failed to delete log file")
|
|
47
102
|
}
|
|
48
103
|
}
|
|
49
104
|
}
|
|
@@ -12,8 +12,8 @@ import ch.qos.logback.core.util.FileSize
|
|
|
12
12
|
import com.margelo.nitro.NitroModules
|
|
13
13
|
import java.io.File
|
|
14
14
|
import java.nio.charset.Charset
|
|
15
|
-
import java.
|
|
16
|
-
import java.
|
|
15
|
+
import java.time.LocalTime
|
|
16
|
+
import java.time.format.DateTimeFormatter
|
|
17
17
|
import java.util.Locale
|
|
18
18
|
|
|
19
19
|
object OneKeyLog {
|
|
@@ -28,6 +28,18 @@ object OneKeyLog {
|
|
|
28
28
|
@Volatile
|
|
29
29
|
private var cachedLogsDir: String? = null
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Initialise OneKeyLog with an Android Context before NitroModules is ready.
|
|
33
|
+
* Call this early in Application.onCreate() so that logs emitted before
|
|
34
|
+
* React Native loads are not silently dropped.
|
|
35
|
+
*/
|
|
36
|
+
@JvmStatic
|
|
37
|
+
fun init(context: android.content.Context) {
|
|
38
|
+
if (cachedLogsDir == null) {
|
|
39
|
+
cachedLogsDir = "${context.cacheDir.absolutePath}/logs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
val logsDirectory: String
|
|
32
44
|
get() {
|
|
33
45
|
cachedLogsDir?.let { return it }
|
|
@@ -99,7 +111,8 @@ object OneKeyLog {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
// DateTimeFormatter is immutable and thread-safe (unlike SimpleDateFormat)
|
|
115
|
+
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss", Locale.US)
|
|
103
116
|
|
|
104
117
|
private fun truncate(message: String): String {
|
|
105
118
|
return if (message.length > MAX_MESSAGE_LENGTH) {
|
|
@@ -109,23 +122,84 @@ object OneKeyLog {
|
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
125
|
+
private fun sanitizeForLog(str: String): String {
|
|
126
|
+
return str.replace("\n", " ").replace("\r", " ")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Sanitize sensitive data from all log messages (native and JS) */
|
|
130
|
+
private val nativeSensitivePatterns = listOf(
|
|
131
|
+
Regex("(?:0x)?[0-9a-fA-F]{64}"),
|
|
132
|
+
Regex("\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b"),
|
|
133
|
+
Regex("\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b"),
|
|
134
|
+
Regex("(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b"),
|
|
135
|
+
Regex("(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}"),
|
|
136
|
+
Regex("(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
private fun sanitizeSensitive(message: String): String {
|
|
140
|
+
var result = message
|
|
141
|
+
for (pattern in nativeSensitivePatterns) {
|
|
142
|
+
result = pattern.replace(result, "[REDACTED]")
|
|
143
|
+
}
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
112
147
|
private fun formatMessage(tag: String, level: String, message: String): String {
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
val safeTag = sanitizeForLog(tag.take(64))
|
|
149
|
+
val safeMessage = sanitizeSensitive(sanitizeForLog(message))
|
|
150
|
+
if (safeTag == "JS") {
|
|
151
|
+
return truncate(safeMessage)
|
|
152
|
+
}
|
|
153
|
+
val time = LocalTime.now().format(timeFormatter)
|
|
154
|
+
return truncate("$time | $level : [$safeTag] $safeMessage")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun log(tag: String, level: String, message: String, androidLogLevel: Int) {
|
|
158
|
+
val formatted = formatMessage(tag, level, message)
|
|
159
|
+
val l = logger
|
|
160
|
+
if (l != null) {
|
|
161
|
+
when (androidLogLevel) {
|
|
162
|
+
android.util.Log.DEBUG -> l.debug(formatted)
|
|
163
|
+
android.util.Log.INFO -> l.info(formatted)
|
|
164
|
+
android.util.Log.WARN -> l.warn(formatted)
|
|
165
|
+
android.util.Log.ERROR -> l.error(formatted)
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// Fallback to android.util.Log when file logger is unavailable
|
|
169
|
+
// Use formatted (sanitized) message, not raw message
|
|
170
|
+
android.util.Log.println(androidLogLevel, "OneKey/${tag.take(64)}", formatted)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Flush all buffered log data to disk.
|
|
176
|
+
* Call before reading log files to ensure the active file is up-to-date.
|
|
177
|
+
*/
|
|
178
|
+
@JvmStatic
|
|
179
|
+
fun flush() {
|
|
180
|
+
val loggerContext = LoggerFactory.getILoggerFactory()
|
|
181
|
+
if (loggerContext is LoggerContext) {
|
|
182
|
+
val appender = loggerContext.getLogger("OneKey")
|
|
183
|
+
?.getAppender(APPENDER_NAME)
|
|
184
|
+
if (appender is RollingFileAppender<*>) {
|
|
185
|
+
try {
|
|
186
|
+
appender.outputStream?.flush()
|
|
187
|
+
} catch (_: Exception) {
|
|
188
|
+
// Ignore flush errors
|
|
189
|
+
}
|
|
190
|
+
}
|
|
115
191
|
}
|
|
116
|
-
val time = timeFormatter.format(Date())
|
|
117
|
-
return truncate("$time | $level : [$tag] $message")
|
|
118
192
|
}
|
|
119
193
|
|
|
120
194
|
@JvmStatic
|
|
121
|
-
fun debug(tag: String, message: String) {
|
|
195
|
+
fun debug(tag: String, message: String) { log(tag, "DEBUG", message, android.util.Log.DEBUG) }
|
|
122
196
|
|
|
123
197
|
@JvmStatic
|
|
124
|
-
fun info(tag: String, message: String) {
|
|
198
|
+
fun info(tag: String, message: String) { log(tag, "INFO", message, android.util.Log.INFO) }
|
|
125
199
|
|
|
126
200
|
@JvmStatic
|
|
127
|
-
fun warn(tag: String, message: String) {
|
|
201
|
+
fun warn(tag: String, message: String) { log(tag, "WARN", message, android.util.Log.WARN) }
|
|
128
202
|
|
|
129
203
|
@JvmStatic
|
|
130
|
-
fun error(tag: String, message: String) {
|
|
204
|
+
fun error(tag: String, message: String) { log(tag, "ERROR", message, android.util.Log.ERROR) }
|
|
131
205
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelogger
|
|
2
|
+
|
|
3
|
+
import android.content.ContentProvider
|
|
4
|
+
import android.content.ContentValues
|
|
5
|
+
import android.database.Cursor
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Auto-initialises [OneKeyLog] before Application.onCreate().
|
|
10
|
+
*
|
|
11
|
+
* ContentProvider.onCreate() is invoked by the system between
|
|
12
|
+
* Application.attachBaseContext() and Application.onCreate(),
|
|
13
|
+
* so the logger is ready for the earliest app-level code.
|
|
14
|
+
*/
|
|
15
|
+
class OneKeyLogInitProvider : ContentProvider() {
|
|
16
|
+
|
|
17
|
+
override fun onCreate(): Boolean {
|
|
18
|
+
context?.let { OneKeyLog.init(it) }
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun query(uri: Uri, proj: Array<String>?, sel: String?, selArgs: Array<String>?, sort: String?): Cursor? = null
|
|
23
|
+
override fun getType(uri: Uri): String? = null
|
|
24
|
+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
25
|
+
override fun delete(uri: Uri, sel: String?, selArgs: Array<String>?): Int = 0
|
|
26
|
+
override fun update(uri: Uri, values: ContentValues?, sel: String?, selArgs: Array<String>?): Int = 0
|
|
27
|
+
}
|
package/ios/NativeLogger.swift
CHANGED
|
@@ -1,31 +1,92 @@
|
|
|
1
1
|
import NitroModules
|
|
2
|
+
import CocoaLumberjack
|
|
2
3
|
|
|
3
4
|
class NativeLogger: HybridNativeLoggerSpec {
|
|
4
5
|
|
|
6
|
+
/// Patterns that should never be written to log files
|
|
7
|
+
private static let sensitivePatterns: [NSRegularExpression] = {
|
|
8
|
+
let patterns = [
|
|
9
|
+
// Hex-encoded private keys (64 hex chars), with optional 0x prefix
|
|
10
|
+
"(?:0x)?[0-9a-fA-F]{64}",
|
|
11
|
+
// WIF private keys (base58, starting with 5, K, or L)
|
|
12
|
+
"\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b",
|
|
13
|
+
// Extended keys (xprv/xpub/zprv/zpub/yprv/ypub)
|
|
14
|
+
"\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b",
|
|
15
|
+
// BIP39 mnemonic-like sequences (12+ words of 3-8 lowercase letters)
|
|
16
|
+
"(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b",
|
|
17
|
+
// Bearer/API tokens
|
|
18
|
+
"(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}",
|
|
19
|
+
// Base64 encoded data that looks like keys (44+ chars)
|
|
20
|
+
"(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}",
|
|
21
|
+
]
|
|
22
|
+
return patterns.compactMap { try? NSRegularExpression(pattern: $0) }
|
|
23
|
+
}()
|
|
24
|
+
|
|
25
|
+
/// Rate limiting: max messages per second
|
|
26
|
+
private static let maxMessagesPerSecond = 100
|
|
27
|
+
private static var messageCount = 0
|
|
28
|
+
private static var windowStart = Date()
|
|
29
|
+
private static let rateLimitLock = NSLock()
|
|
30
|
+
|
|
31
|
+
private static func isRateLimited() -> Bool {
|
|
32
|
+
rateLimitLock.lock()
|
|
33
|
+
defer { rateLimitLock.unlock() }
|
|
34
|
+
let now = Date()
|
|
35
|
+
if now.timeIntervalSince(windowStart) >= 1.0 {
|
|
36
|
+
windowStart = now
|
|
37
|
+
messageCount = 0
|
|
38
|
+
}
|
|
39
|
+
messageCount += 1
|
|
40
|
+
return messageCount > maxMessagesPerSecond
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static func sanitize(_ message: String) -> String {
|
|
44
|
+
var result = message
|
|
45
|
+
for regex in sensitivePatterns {
|
|
46
|
+
result = regex.stringByReplacingMatches(
|
|
47
|
+
in: result,
|
|
48
|
+
range: NSRange(result.startIndex..., in: result),
|
|
49
|
+
withTemplate: "[REDACTED]"
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
// Strip newlines to prevent log injection
|
|
53
|
+
result = result.replacingOccurrences(of: "\n", with: " ")
|
|
54
|
+
result = result.replacingOccurrences(of: "\r", with: " ")
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
5
58
|
func write(level: Double, msg: String) {
|
|
59
|
+
if NativeLogger.isRateLimited() { return }
|
|
60
|
+
let sanitized = NativeLogger.sanitize(msg)
|
|
6
61
|
switch Int(level) {
|
|
7
|
-
case 0: OneKeyLog.debug("JS",
|
|
8
|
-
case 1: OneKeyLog.info("JS",
|
|
9
|
-
case 2: OneKeyLog.warn("JS",
|
|
10
|
-
case 3: OneKeyLog.error("JS",
|
|
11
|
-
default: OneKeyLog.info("JS",
|
|
62
|
+
case 0: OneKeyLog.debug("JS", sanitized)
|
|
63
|
+
case 1: OneKeyLog.info("JS", sanitized)
|
|
64
|
+
case 2: OneKeyLog.warn("JS", sanitized)
|
|
65
|
+
case 3: OneKeyLog.error("JS", sanitized)
|
|
66
|
+
default: OneKeyLog.info("JS", sanitized)
|
|
12
67
|
}
|
|
13
68
|
}
|
|
14
69
|
|
|
70
|
+
func getLogDirectory() throws -> String {
|
|
71
|
+
return OneKeyLog.logsDirectory
|
|
72
|
+
}
|
|
73
|
+
|
|
15
74
|
func getLogFilePaths() throws -> Promise<[String]> {
|
|
16
75
|
return Promise.async {
|
|
76
|
+
// Flush buffered log data so the active file reflects all written logs
|
|
77
|
+
DDLog.flushLog()
|
|
17
78
|
let dir = OneKeyLog.logsDirectory
|
|
18
79
|
let fm = FileManager.default
|
|
19
80
|
let files: [String]
|
|
20
81
|
do {
|
|
21
82
|
files = try fm.contentsOfDirectory(atPath: dir)
|
|
22
83
|
} catch {
|
|
23
|
-
OneKeyLog.warn("NativeLogger", "Failed to list log directory
|
|
84
|
+
OneKeyLog.warn("NativeLogger", "Failed to list log directory")
|
|
24
85
|
return []
|
|
25
86
|
}
|
|
87
|
+
// Return filenames only, not absolute paths
|
|
26
88
|
return files
|
|
27
89
|
.filter { $0.hasSuffix(".log") }
|
|
28
|
-
.map { "\(dir)/\($0)" }
|
|
29
90
|
.sorted()
|
|
30
91
|
}
|
|
31
92
|
}
|
|
@@ -38,7 +99,7 @@ class NativeLogger: HybridNativeLoggerSpec {
|
|
|
38
99
|
do {
|
|
39
100
|
files = try fm.contentsOfDirectory(atPath: dir)
|
|
40
101
|
} catch {
|
|
41
|
-
OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion
|
|
102
|
+
OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion")
|
|
42
103
|
return
|
|
43
104
|
}
|
|
44
105
|
// Skip the active log file to avoid breaking CocoaLumberjack's open file handle
|
|
@@ -46,7 +107,7 @@ class NativeLogger: HybridNativeLoggerSpec {
|
|
|
46
107
|
do {
|
|
47
108
|
try fm.removeItem(atPath: "\(dir)/\(file)")
|
|
48
109
|
} catch {
|
|
49
|
-
OneKeyLog.warn("NativeLogger", "Failed to delete log file
|
|
110
|
+
OneKeyLog.warn("NativeLogger", "Failed to delete log file")
|
|
50
111
|
}
|
|
51
112
|
}
|
|
52
113
|
}
|
package/ios/OneKeyLog.swift
CHANGED
|
@@ -5,13 +5,20 @@ private let ddLogLevel: DDLogLevel = .debug
|
|
|
5
5
|
private class OneKeyLogFileManager: DDLogFileManagerDefault {
|
|
6
6
|
private static let logPrefix = "app"
|
|
7
7
|
private static let latestFileName = "\(logPrefix)-latest.log"
|
|
8
|
-
private static let
|
|
8
|
+
private static let dateFormatterLock = NSLock()
|
|
9
|
+
private static let _dateFormatter: DateFormatter = {
|
|
9
10
|
let fmt = DateFormatter()
|
|
10
11
|
fmt.dateFormat = "yyyy-MM-dd"
|
|
11
12
|
fmt.locale = Locale(identifier: "en_US_POSIX")
|
|
12
13
|
return fmt
|
|
13
14
|
}()
|
|
14
15
|
|
|
16
|
+
private static func formattedDate(_ date: Date) -> String {
|
|
17
|
+
dateFormatterLock.lock()
|
|
18
|
+
defer { dateFormatterLock.unlock() }
|
|
19
|
+
return _dateFormatter.string(from: date)
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/// Always write to app-latest.log (matches Android behavior)
|
|
16
23
|
override var newLogFileName: String {
|
|
17
24
|
return Self.latestFileName
|
|
@@ -23,33 +30,40 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
|
|
|
23
30
|
|
|
24
31
|
/// When rolled, rename app-latest.log → app-{yyyy-MM-dd}.{i}.log (matches Android pattern)
|
|
25
32
|
override func didArchiveLogFile(atPath logFilePath: String, wasRolled: Bool) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Another thread may have created this file; try next index
|
|
45
|
-
index += 1
|
|
46
|
-
}
|
|
33
|
+
guard wasRolled else {
|
|
34
|
+
super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let dir = (logFilePath as NSString).deletingLastPathComponent
|
|
39
|
+
let dateStr = Self.formattedDate(Date())
|
|
40
|
+
let fm = FileManager.default
|
|
41
|
+
|
|
42
|
+
// Find next available index, retry on move failure to handle TOCTOU race
|
|
43
|
+
var index = 0
|
|
44
|
+
var archivedPath = logFilePath
|
|
45
|
+
var moved = false
|
|
46
|
+
while !moved && index < 1000 {
|
|
47
|
+
archivedPath = "\(dir)/\(Self.logPrefix)-\(dateStr).\(index).log"
|
|
48
|
+
if fm.fileExists(atPath: archivedPath) {
|
|
49
|
+
index += 1
|
|
50
|
+
continue
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
do {
|
|
53
|
+
try fm.moveItem(atPath: logFilePath, toPath: archivedPath)
|
|
54
|
+
moved = true
|
|
55
|
+
} catch {
|
|
56
|
+
// Another thread may have created this file; try next index
|
|
57
|
+
index += 1
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
|
-
|
|
60
|
+
if !moved {
|
|
61
|
+
NSLog("[OneKeyLog] Failed to archive log file after 1000 attempts: %@", logFilePath)
|
|
62
|
+
super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled)
|
|
63
|
+
} else {
|
|
64
|
+
// Pass the new path so the super class can find the file for cleanup
|
|
65
|
+
super.didArchiveLogFile(atPath: archivedPath, wasRolled: wasRolled)
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
|
|
@@ -86,13 +100,21 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
|
|
|
86
100
|
return true
|
|
87
101
|
}()
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
// Use a lock to protect DateFormatter (not thread-safe per Apple docs)
|
|
104
|
+
private static let timeFormatterLock = NSLock()
|
|
105
|
+
private static let _timeFormatter: DateFormatter = {
|
|
90
106
|
let fmt = DateFormatter()
|
|
91
107
|
fmt.dateFormat = "HH:mm:ss"
|
|
92
108
|
fmt.locale = Locale(identifier: "en_US_POSIX")
|
|
93
109
|
return fmt
|
|
94
110
|
}()
|
|
95
111
|
|
|
112
|
+
private static func formattedTime(_ date: Date) -> String {
|
|
113
|
+
timeFormatterLock.lock()
|
|
114
|
+
defer { timeFormatterLock.unlock() }
|
|
115
|
+
return _timeFormatter.string(from: date)
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
private static func truncate(_ message: String) -> String {
|
|
97
119
|
if message.count > maxMessageLength {
|
|
98
120
|
return String(message.prefix(maxMessageLength)) + "...(truncated)"
|
|
@@ -100,12 +122,44 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
|
|
|
100
122
|
return message
|
|
101
123
|
}
|
|
102
124
|
|
|
125
|
+
private static func sanitizeForLog(_ str: String) -> String {
|
|
126
|
+
return str.replacingOccurrences(of: "\n", with: " ")
|
|
127
|
+
.replacingOccurrences(of: "\r", with: " ")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Sanitize sensitive data from native-side log messages
|
|
131
|
+
private static let nativeSensitivePatterns: [NSRegularExpression] = {
|
|
132
|
+
let patterns = [
|
|
133
|
+
"(?:0x)?[0-9a-fA-F]{64}",
|
|
134
|
+
"\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b",
|
|
135
|
+
"\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b",
|
|
136
|
+
"(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b",
|
|
137
|
+
"(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}",
|
|
138
|
+
"(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}",
|
|
139
|
+
]
|
|
140
|
+
return patterns.compactMap { try? NSRegularExpression(pattern: $0) }
|
|
141
|
+
}()
|
|
142
|
+
|
|
143
|
+
private static func sanitizeSensitive(_ message: String) -> String {
|
|
144
|
+
var result = message
|
|
145
|
+
for regex in nativeSensitivePatterns {
|
|
146
|
+
result = regex.stringByReplacingMatches(
|
|
147
|
+
in: result,
|
|
148
|
+
range: NSRange(result.startIndex..., in: result),
|
|
149
|
+
withTemplate: "[REDACTED]"
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
|
|
103
155
|
private static func formatMessage(_ tag: String, _ level: String, _ message: String) -> String {
|
|
104
|
-
|
|
105
|
-
|
|
156
|
+
let safeTag = sanitizeForLog(String(tag.prefix(64)))
|
|
157
|
+
let safeMessage = sanitizeSensitive(sanitizeForLog(message))
|
|
158
|
+
if safeTag == "JS" {
|
|
159
|
+
return truncate(safeMessage)
|
|
106
160
|
}
|
|
107
|
-
let time =
|
|
108
|
-
return truncate("\(time) | \(level) : [\(
|
|
161
|
+
let time = formattedTime(Date())
|
|
162
|
+
return truncate("\(time) | \(level) : [\(safeTag)] \(safeMessage)")
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
@objc public static func debug(_ tag: String, _ message: String) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeLogger.nitro.d.ts","sourceRoot":"","sources":["../../../src/NativeLogger.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,YACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC"}
|
|
1
|
+
{"version":3,"file":"NativeLogger.nitro.d.ts","sourceRoot":"","sources":["../../../src/NativeLogger.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,YACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,eAAe,IAAI,MAAM,CAAC;IAC1B,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC"}
|
|
@@ -50,6 +50,11 @@ namespace margelo::nitro::nativelogger {
|
|
|
50
50
|
static const auto method = javaClassStatic()->getMethod<void(double /* level */, jni::alias_ref<jni::JString> /* msg */)>("write");
|
|
51
51
|
method(_javaPart, level, jni::make_jstring(msg));
|
|
52
52
|
}
|
|
53
|
+
std::string JHybridNativeLoggerSpec::getLogDirectory() {
|
|
54
|
+
static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>()>("getLogDirectory");
|
|
55
|
+
auto __result = method(_javaPart);
|
|
56
|
+
return __result->toStdString();
|
|
57
|
+
}
|
|
53
58
|
std::shared_ptr<Promise<std::vector<std::string>>> JHybridNativeLoggerSpec::getLogFilePaths() {
|
|
54
59
|
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("getLogFilePaths");
|
|
55
60
|
auto __result = method(_javaPart);
|
|
@@ -55,6 +55,7 @@ namespace margelo::nitro::nativelogger {
|
|
|
55
55
|
public:
|
|
56
56
|
// Methods
|
|
57
57
|
void write(double level, const std::string& msg) override;
|
|
58
|
+
std::string getLogDirectory() override;
|
|
58
59
|
std::shared_ptr<Promise<std::vector<std::string>>> getLogFilePaths() override;
|
|
59
60
|
std::shared_ptr<Promise<void>> deleteLogFiles() override;
|
|
60
61
|
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelogger/HybridNativeLoggerSpec.kt
CHANGED
|
@@ -50,6 +50,10 @@ abstract class HybridNativeLoggerSpec: HybridObject() {
|
|
|
50
50
|
@Keep
|
|
51
51
|
abstract fun write(level: Double, msg: String): Unit
|
|
52
52
|
|
|
53
|
+
@DoNotStrip
|
|
54
|
+
@Keep
|
|
55
|
+
abstract fun getLogDirectory(): String
|
|
56
|
+
|
|
53
57
|
@DoNotStrip
|
|
54
58
|
@Keep
|
|
55
59
|
abstract fun getLogFilePaths(): Promise<Array<String>>
|
|
@@ -154,6 +154,15 @@ namespace margelo::nitro::nativelogger::bridge::swift {
|
|
|
154
154
|
return Result<void>::withError(error);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// pragma MARK: Result<std::string>
|
|
158
|
+
using Result_std__string_ = Result<std::string>;
|
|
159
|
+
inline Result_std__string_ create_Result_std__string_(const std::string& value) noexcept {
|
|
160
|
+
return Result<std::string>::withValue(value);
|
|
161
|
+
}
|
|
162
|
+
inline Result_std__string_ create_Result_std__string_(const std::exception_ptr& error) noexcept {
|
|
163
|
+
return Result<std::string>::withError(error);
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
// pragma MARK: Result<std::shared_ptr<Promise<std::vector<std::string>>>>
|
|
158
167
|
using Result_std__shared_ptr_Promise_std__vector_std__string____ = Result<std::shared_ptr<Promise<std::vector<std::string>>>>;
|
|
159
168
|
inline Result_std__shared_ptr_Promise_std__vector_std__string____ create_Result_std__shared_ptr_Promise_std__vector_std__string____(const std::shared_ptr<Promise<std::vector<std::string>>>& value) noexcept {
|
|
@@ -68,6 +68,14 @@ namespace margelo::nitro::nativelogger {
|
|
|
68
68
|
std::rethrow_exception(__result.error());
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
inline std::string getLogDirectory() override {
|
|
72
|
+
auto __result = _swiftPart.getLogDirectory();
|
|
73
|
+
if (__result.hasError()) [[unlikely]] {
|
|
74
|
+
std::rethrow_exception(__result.error());
|
|
75
|
+
}
|
|
76
|
+
auto __value = std::move(__result.value());
|
|
77
|
+
return __value;
|
|
78
|
+
}
|
|
71
79
|
inline std::shared_ptr<Promise<std::vector<std::string>>> getLogFilePaths() override {
|
|
72
80
|
auto __result = _swiftPart.getLogFilePaths();
|
|
73
81
|
if (__result.hasError()) [[unlikely]] {
|
|
@@ -15,6 +15,7 @@ public protocol HybridNativeLoggerSpec_protocol: HybridObject {
|
|
|
15
15
|
|
|
16
16
|
// Methods
|
|
17
17
|
func write(level: Double, msg: String) throws -> Void
|
|
18
|
+
func getLogDirectory() throws -> String
|
|
18
19
|
func getLogFilePaths() throws -> Promise<[String]>
|
|
19
20
|
func deleteLogFiles() throws -> Promise<Void>
|
|
20
21
|
}
|
|
@@ -128,6 +128,18 @@ open class HybridNativeLoggerSpec_cxx {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
@inline(__always)
|
|
132
|
+
public final func getLogDirectory() -> bridge.Result_std__string_ {
|
|
133
|
+
do {
|
|
134
|
+
let __result = try self.__implementation.getLogDirectory()
|
|
135
|
+
let __resultCpp = std.string(__result)
|
|
136
|
+
return bridge.create_Result_std__string_(__resultCpp)
|
|
137
|
+
} catch (let __error) {
|
|
138
|
+
let __exceptionPtr = __error.toCpp()
|
|
139
|
+
return bridge.create_Result_std__string_(__exceptionPtr)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
@inline(__always)
|
|
132
144
|
public final func getLogFilePaths() -> bridge.Result_std__shared_ptr_Promise_std__vector_std__string____ {
|
|
133
145
|
do {
|
|
@@ -15,6 +15,7 @@ namespace margelo::nitro::nativelogger {
|
|
|
15
15
|
// load custom methods/properties
|
|
16
16
|
registerHybrids(this, [](Prototype& prototype) {
|
|
17
17
|
prototype.registerHybridMethod("write", &HybridNativeLoggerSpec::write);
|
|
18
|
+
prototype.registerHybridMethod("getLogDirectory", &HybridNativeLoggerSpec::getLogDirectory);
|
|
18
19
|
prototype.registerHybridMethod("getLogFilePaths", &HybridNativeLoggerSpec::getLogFilePaths);
|
|
19
20
|
prototype.registerHybridMethod("deleteLogFiles", &HybridNativeLoggerSpec::deleteLogFiles);
|
|
20
21
|
});
|
|
@@ -51,6 +51,7 @@ namespace margelo::nitro::nativelogger {
|
|
|
51
51
|
public:
|
|
52
52
|
// Methods
|
|
53
53
|
virtual void write(double level, const std::string& msg) = 0;
|
|
54
|
+
virtual std::string getLogDirectory() = 0;
|
|
54
55
|
virtual std::shared_ptr<Promise<std::vector<std::string>>> getLogFilePaths() = 0;
|
|
55
56
|
virtual std::shared_ptr<Promise<void>> deleteLogFiles() = 0;
|
|
56
57
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import type { HybridObject } from 'react-native-nitro-modules';
|
|
|
3
3
|
export interface NativeLogger
|
|
4
4
|
extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
|
|
5
5
|
write(level: number, msg: string): void;
|
|
6
|
+
getLogDirectory(): string;
|
|
6
7
|
getLogFilePaths(): Promise<string[]>;
|
|
7
8
|
deleteLogFiles(): Promise<void>;
|
|
8
9
|
}
|