@onekeyfe/react-native-sni-connect 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Leon
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # react-native-sni-connect
2
+
3
+ onekey sni http client
4
+
5
+ ## Installation
6
+
7
+
8
+ ```sh
9
+ npm install react-native-sni-connect
10
+ ```
11
+
12
+
13
+ ## Usage
14
+
15
+
16
+ ```js
17
+ import { multiply } from 'react-native-sni-connect';
18
+
19
+ // ...
20
+
21
+ const result = multiply(3, 7);
22
+ ```
23
+
24
+
25
+ ## Contributing
26
+
27
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
28
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
29
+ - [Code of conduct](CODE_OF_CONDUCT.md)
30
+
31
+ ## License
32
+
33
+ MIT
34
+
35
+ ---
36
+
37
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,36 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "SniConnect"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/OneKeyHQ/react-native-sni-connect.git", :tag => "#{s.version}" }
15
+
16
+ s.static_framework = true
17
+
18
+ s.source_files = [
19
+ "ios/**/*.{swift}",
20
+ "ios/**/*.{m,mm}"
21
+ ]
22
+
23
+ s.dependency 'React-Core'
24
+ s.dependency 'React-jsi'
25
+ s.dependency 'React-callinvoker'
26
+ s.dependency 'React-Codegen'
27
+ s.dependency 'EMASCurl', '1.4.1'
28
+
29
+ s.pod_target_xcconfig = {
30
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\"",
31
+ "DEFINES_MODULE" => "YES"
32
+ }
33
+
34
+
35
+ install_modules_dependencies(s)
36
+ end
@@ -0,0 +1,89 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['SniConnect_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+ apply plugin: "com.facebook.react"
22
+
23
+ def getExtOrIntegerDefault(name) {
24
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["SniConnect_" + name]).toInteger()
25
+ }
26
+
27
+ android {
28
+ namespace "com.sniconnect"
29
+
30
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
31
+
32
+ defaultConfig {
33
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
34
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
35
+ }
36
+
37
+ packagingOptions {
38
+ excludes = [
39
+ "META-INF",
40
+ "META-INF/**",
41
+ "**/libc++_shared.so",
42
+ "**/libfbjni.so",
43
+ "**/libjsi.so",
44
+ "**/libfolly_json.so",
45
+ "**/libfolly_runtime.so",
46
+ "**/libglog.so",
47
+ "**/libhermes.so",
48
+ "**/libhermes-executor-debug.so",
49
+ "**/libhermes_executor.so",
50
+ "**/libreactnative.so",
51
+ "**/libreactnativejni.so",
52
+ "**/libturbomodulejsijni.so",
53
+ "**/libreact_nativemodule_core.so",
54
+ "**/libjscexecutor.so"
55
+ ]
56
+ }
57
+
58
+ buildFeatures {
59
+ buildConfig true
60
+ }
61
+
62
+ buildTypes {
63
+ release {
64
+ minifyEnabled false
65
+ }
66
+ }
67
+
68
+ lintOptions {
69
+ disable "GradleCompatible"
70
+ }
71
+
72
+ compileOptions {
73
+ sourceCompatibility JavaVersion.VERSION_1_8
74
+ targetCompatibility JavaVersion.VERSION_1_8
75
+ }
76
+ }
77
+
78
+ repositories {
79
+ mavenCentral()
80
+ google()
81
+ }
82
+
83
+ def kotlin_version = getExtOrDefault("kotlinVersion")
84
+
85
+ dependencies {
86
+ implementation "com.facebook.react:react-android"
87
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
88
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
89
+ }
@@ -0,0 +1,5 @@
1
+ SniConnect_kotlinVersion=2.0.21
2
+ SniConnect_minSdkVersion=24
3
+ SniConnect_targetSdkVersion=34
4
+ SniConnect_compileSdkVersion=35
5
+ SniConnect_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,346 @@
1
+ package com.sniconnect
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReactMethod
8
+ import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.modules.core.DeviceEventManagerModule
11
+ import com.facebook.react.module.annotations.ReactModule
12
+ import okhttp3.Call
13
+ import okhttp3.Callback
14
+ import okhttp3.Dns
15
+ import okhttp3.Headers
16
+ import okhttp3.MediaType.Companion.toMediaTypeOrNull
17
+ import okhttp3.OkHttpClient
18
+ import okhttp3.Request
19
+ import okhttp3.RequestBody.Companion.toRequestBody
20
+ import okhttp3.Response
21
+ import okhttp3.ResponseBody
22
+ import java.io.IOException
23
+ import java.net.InetAddress
24
+ import java.net.UnknownHostException
25
+ import java.util.Locale
26
+ import java.util.concurrent.ConcurrentHashMap
27
+ import java.util.concurrent.TimeUnit
28
+ import javax.net.ssl.HttpsURLConnection
29
+
30
+ private const val TAG = "SniConnect"
31
+
32
+ @ReactModule(name = SniConnectModule.NAME)
33
+ class SniConnectModule(reactContext: ReactApplicationContext) :
34
+ NativeSniConnectSpec(reactContext) {
35
+
36
+ companion object {
37
+ const val NAME = "SniConnect"
38
+ private const val EVENT_NAME = "SniConnectLog"
39
+ }
40
+
41
+ private data class ClientKey(
42
+ val ip: String,
43
+ val hostname: String,
44
+ val timeoutMillis: Long,
45
+ )
46
+
47
+ private val clientCache = ConcurrentHashMap<ClientKey, OkHttpClient>()
48
+ private val activeCalls = ConcurrentHashMap<String, Call>()
49
+ private var listenerCount = 0
50
+
51
+ override fun getName(): String = NAME
52
+
53
+ // Event emitter methods required for NativeEventEmitter
54
+ @ReactMethod
55
+ override fun addListener(eventType: String) {
56
+ listenerCount += 1
57
+ }
58
+
59
+ @ReactMethod
60
+ override fun removeListeners(count: Double) {
61
+ listenerCount -= count.toInt()
62
+ if (listenerCount < 0) {
63
+ listenerCount = 0
64
+ }
65
+ }
66
+
67
+ private fun sendLogEvent(level: String, message: String) {
68
+ if (listenerCount > 0) {
69
+ val logData = Arguments.createMap().apply {
70
+ putString("level", level)
71
+ putString("message", message)
72
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
73
+ }
74
+
75
+ reactApplicationContext
76
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
77
+ ?.emit(EVENT_NAME, logData)
78
+ }
79
+ }
80
+
81
+ override fun request(config: ReadableMap, promise: Promise) {
82
+ try {
83
+ val requestConfig = config.toRequestConfig()
84
+ // Log module initialization
85
+ sendLogEvent("info", "SniConnect module initialized successfully")
86
+ performRequest(requestConfig, promise)
87
+ } catch (error: Exception) {
88
+ Log.e(TAG, "[SniConnect] Request failed", error)
89
+ sendLogEvent("error", "Config parsing failed: ${error.message}")
90
+ promise.reject("SNI_REQUEST_FAILED", error.message, error)
91
+ }
92
+ }
93
+
94
+ @ReactMethod
95
+ override fun cancelRequest(requestId: String, promise: Promise) {
96
+ val call = activeCalls.remove(requestId)
97
+ if (call != null) {
98
+ call.cancel()
99
+ sendLogEvent("info", "Cancelled request: $requestId")
100
+ promise.resolve(Arguments.createMap().apply {
101
+ putBoolean("success", true)
102
+ })
103
+ } else {
104
+ sendLogEvent("info", "Request not found: $requestId")
105
+ promise.resolve(Arguments.createMap().apply {
106
+ putBoolean("success", false)
107
+ })
108
+ }
109
+ }
110
+
111
+ @ReactMethod
112
+ override fun cancelAllRequests(promise: Promise) {
113
+ val count = activeCalls.size
114
+ activeCalls.forEach { (_, call) ->
115
+ call.cancel()
116
+ }
117
+ activeCalls.clear()
118
+ sendLogEvent("info", "Cancelled $count active requests")
119
+ promise.resolve(Arguments.createMap().apply {
120
+ putBoolean("success", true)
121
+ })
122
+ }
123
+
124
+ @ReactMethod
125
+ override fun clearDNSCache(promise: Promise) {
126
+ clientCache.clear()
127
+ sendLogEvent("info", "DNS cache cleared")
128
+ promise.resolve(Arguments.createMap().apply {
129
+ putBoolean("success", true)
130
+ })
131
+ }
132
+
133
+ private fun performRequest(config: RequestConfig, promise: Promise) {
134
+ try {
135
+ val client = getOrCreateClient(config)
136
+ val request = buildRequest(config)
137
+ val call = client.newCall(request)
138
+
139
+ // Register the call if requestId is provided
140
+ config.requestId?.let { requestId ->
141
+ activeCalls[requestId] = call
142
+ sendLogEvent("info", "Registered request: $requestId, total active: ${activeCalls.size}")
143
+ }
144
+
145
+ call.enqueue(object : Callback {
146
+ override fun onFailure(call: Call, e: IOException) {
147
+ // Unregister the call
148
+ config.requestId?.let { activeCalls.remove(it) }
149
+
150
+ if (call.isCanceled()) {
151
+ sendLogEvent("info", "Request cancelled")
152
+ promise.reject("SNI_CANCELLED", "Request cancelled", null)
153
+ } else {
154
+ Log.e(TAG, "[SniConnect] Request failed", e)
155
+ sendLogEvent("error", "Request failed: ${e.message}")
156
+ promise.reject("SNI_REQUEST_FAILED", e.message, e)
157
+ }
158
+ }
159
+
160
+ override fun onResponse(call: Call, response: Response) {
161
+ // Unregister the call
162
+ config.requestId?.let { activeCalls.remove(it) }
163
+
164
+ try {
165
+ response.use {
166
+ val bodyString = response.body.safeString()
167
+ val headerMap = headersToMap(response.headers)
168
+
169
+ val result: WritableMap = Arguments.createMap().apply {
170
+ putString("data", bodyString)
171
+ putInt("status", response.code)
172
+ putString("statusText", response.message)
173
+ putMap("headers", headerMap.toWritableMap())
174
+ }
175
+
176
+ promise.resolve(result)
177
+ }
178
+ } catch (error: Exception) {
179
+ Log.e(TAG, "[SniConnect] Response processing failed", error)
180
+ sendLogEvent("error", "Response processing failed: ${error.message}")
181
+ promise.reject("SNI_RESPONSE_FAILED", error.message, error)
182
+ }
183
+ }
184
+ })
185
+ } catch (error: Exception) {
186
+ Log.e(TAG, "[SniConnect] Request setup failed", error)
187
+ sendLogEvent("error", "Request setup failed: ${error.message}")
188
+ promise.reject("SNI_REQUEST_FAILED", error.message, error)
189
+ }
190
+ }
191
+
192
+ private fun getOrCreateClient(config: RequestConfig): OkHttpClient {
193
+ val normalizedHost = config.hostname.lowercase(Locale.US)
194
+ val key = ClientKey(config.ip, normalizedHost, config.timeoutMillis)
195
+
196
+ return clientCache.getOrPut(key) {
197
+ OkHttpClient.Builder()
198
+ .connectTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
199
+ .readTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
200
+ .writeTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
201
+ .callTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
202
+ .hostnameVerifier { _, session ->
203
+ HttpsURLConnection.getDefaultHostnameVerifier().verify(config.hostname, session)
204
+ }
205
+ .dns(createPinnedDns(config.ip, config.hostname))
206
+ .build()
207
+ }
208
+ }
209
+
210
+ private fun createPinnedDns(ip: String, hostname: String): Dns =
211
+ object : Dns {
212
+ private val expectedHost = hostname.lowercase(Locale.US)
213
+
214
+ override fun lookup(requestedHost: String): List<InetAddress> {
215
+ return if (requestedHost.lowercase(Locale.US) == expectedHost) {
216
+ listOf(resolveIp(ip))
217
+ } else {
218
+ Dns.SYSTEM.lookup(requestedHost)
219
+ }
220
+ }
221
+ }
222
+
223
+ private fun buildRequest(config: RequestConfig): Request {
224
+ val normalizedPath = if (config.path.startsWith("http")) {
225
+ config.path
226
+ } else {
227
+ val prefix = if (config.path.startsWith("/")) "" else "/"
228
+ "https://${config.hostname}$prefix${config.path}"
229
+ }
230
+
231
+ val builder = Request.Builder().url(normalizedPath)
232
+
233
+ config.headers.forEach { (key, value) ->
234
+ if (!key.equals("host", ignoreCase = true)) {
235
+ builder.addHeader(key, value)
236
+ }
237
+ }
238
+ builder.header("Host", config.hostname)
239
+
240
+ val method = config.method.uppercase(Locale.US)
241
+ val bodyContent = config.body ?: ""
242
+ val mediaType = config.headers.entries
243
+ .firstOrNull { it.key.equals("Content-Type", ignoreCase = true) }
244
+ ?.value
245
+ ?.toMediaTypeOrNull()
246
+ ?: "application/json; charset=utf-8".toMediaTypeOrNull()
247
+
248
+ when (method) {
249
+ "GET" -> builder.get()
250
+ "HEAD" -> builder.head()
251
+ else -> {
252
+ val requestBody = bodyContent.toRequestBody(mediaType)
253
+ when (method) {
254
+ "POST" -> builder.post(requestBody)
255
+ "PUT" -> builder.put(requestBody)
256
+ "PATCH" -> builder.patch(requestBody)
257
+ "DELETE" -> builder.delete(requestBody)
258
+ else -> builder.method(method, requestBody)
259
+ }
260
+ }
261
+ }
262
+
263
+ return builder.build()
264
+ }
265
+
266
+ private fun ResponseBody?.safeString(): String {
267
+ if (this == null) {
268
+ return ""
269
+ }
270
+ return try {
271
+ this.string()
272
+ } catch (error: IOException) {
273
+ Log.e(TAG, "[SniConnect] Failed to read response body", error)
274
+ throw IOException("Failed to read response body", error)
275
+ }
276
+ }
277
+
278
+ private fun headersToMap(headers: Headers): Map<String, String> {
279
+ val map = mutableMapOf<String, String>()
280
+ for (name in headers.names()) {
281
+ map[name] = headers[name] ?: ""
282
+ }
283
+ return map
284
+ }
285
+
286
+ private fun resolveIp(ip: String): InetAddress {
287
+ return try {
288
+ InetAddress.getByName(ip)
289
+ } catch (error: UnknownHostException) {
290
+ throw IOException("Invalid IP address: $ip", error)
291
+ }
292
+ }
293
+
294
+ private fun Map<String, String>.toWritableMap(): WritableMap {
295
+ return Arguments.createMap().apply {
296
+ forEach { (key, value) ->
297
+ putString(key, value)
298
+ }
299
+ }
300
+ }
301
+
302
+ private fun ReadableMap.toRequestConfig(): RequestConfig {
303
+ val headersMap = if (hasKey("headers") && !isNull("headers")) {
304
+ val headersReadable = getMap("headers")
305
+ headersReadable?.toHashMap()
306
+ ?.mapValues { (_, value) -> value?.toString() ?: "" }
307
+ ?: emptyMap()
308
+ } else {
309
+ emptyMap()
310
+ }
311
+
312
+ val timeoutMillis = if (hasKey("timeout")) {
313
+ (getDouble("timeout") * 1.0).toLong()
314
+ } else {
315
+ 30_000L
316
+ }
317
+
318
+ val requestId = if (hasKey("requestId") && !isNull("requestId")) {
319
+ getString("requestId")
320
+ } else {
321
+ null
322
+ }
323
+
324
+ return RequestConfig(
325
+ requestId = requestId,
326
+ ip = getString("ip") ?: throw IllegalArgumentException("ip is required"),
327
+ hostname = getString("hostname") ?: throw IllegalArgumentException("hostname is required"),
328
+ method = getString("method") ?: "GET",
329
+ path = getString("path") ?: "/",
330
+ headers = headersMap,
331
+ body = if (hasKey("body") && !isNull("body")) getString("body") else null,
332
+ timeoutMillis = timeoutMillis,
333
+ )
334
+ }
335
+
336
+ private data class RequestConfig(
337
+ val requestId: String?,
338
+ val ip: String,
339
+ val hostname: String,
340
+ val method: String,
341
+ val path: String,
342
+ val headers: Map<String, String>,
343
+ val body: String?,
344
+ val timeoutMillis: Long,
345
+ )
346
+ }
@@ -0,0 +1,31 @@
1
+ package com.sniconnect
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ class SniConnectPackage : TurboReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12
+ if (name == SniConnectModule.NAME) SniConnectModule(reactContext) else null
13
+
14
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
15
+ ReactModuleInfoProvider {
16
+ mapOf(
17
+ SniConnectModule.NAME to ReactModuleInfo(
18
+ SniConnectModule.NAME,
19
+ SniConnectModule.NAME,
20
+ false,
21
+ false,
22
+ true,
23
+ false,
24
+ true
25
+ )
26
+ )
27
+ }
28
+
29
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
30
+ emptyList()
31
+ }
@@ -0,0 +1,13 @@
1
+ //
2
+ // SniConnect-Bridging-Header.h
3
+ // SniConnect
4
+ //
5
+ // Used to import Objective-C headers that should be visible to Swift.
6
+ //
7
+
8
+ #import <React/RCTBridgeModule.h>
9
+ #import <React/RCTEventEmitter.h>
10
+
11
+ #ifdef RCT_NEW_ARCH_ENABLED
12
+ #import <SniConnectSpec/SniConnectSpec.h>
13
+ #endif