@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 +20 -0
- package/README.md +37 -0
- package/SniConnect.podspec +36 -0
- package/android/build.gradle +89 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +346 -0
- package/android/src/main/java/com/sniconnect/SniConnectPackage.kt +31 -0
- package/ios/SniConnect-Bridging-Header.h +13 -0
- package/ios/SniConnect.mm +141 -0
- package/ios/SniConnect.swift +185 -0
- package/ios/SniConnectClient.swift +490 -0
- package/lib/module/@types/react-native-codegen.d.js +2 -0
- package/lib/module/@types/react-native-codegen.d.js.map +1 -0
- package/lib/module/NativeSniConnect.js +17 -0
- package/lib/module/NativeSniConnect.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSniConnect.d.ts +50 -0
- package/lib/typescript/src/NativeSniConnect.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +167 -0
- package/src/@types/react-native-codegen.d.ts +8 -0
- package/src/NativeSniConnect.ts +70 -0
- package/src/index.tsx +46 -0
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,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
|