@onekeyfe/react-native-sni-connect 1.0.0 → 1.1.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.
|
@@ -38,10 +38,16 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
|
|
|
38
38
|
private const val EVENT_NAME = "SniConnectLog"
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Cache key for OkHttpClient instances
|
|
43
|
+
* Uses hostname:IP combination to ensure:
|
|
44
|
+
* - Different IPs for the same hostname are isolated (accurate speed testing)
|
|
45
|
+
* - Same hostname+IP combination can reuse connections (performance optimization)
|
|
46
|
+
* Note: timeout is NOT part of the key to allow connection reuse across different timeout values
|
|
47
|
+
*/
|
|
41
48
|
private data class ClientKey(
|
|
42
|
-
val ip: String,
|
|
43
49
|
val hostname: String,
|
|
44
|
-
val
|
|
50
|
+
val ip: String,
|
|
45
51
|
)
|
|
46
52
|
|
|
47
53
|
private val clientCache = ConcurrentHashMap<ClientKey, OkHttpClient>()
|
|
@@ -136,6 +142,9 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
|
|
|
136
142
|
val request = buildRequest(config)
|
|
137
143
|
val call = client.newCall(request)
|
|
138
144
|
|
|
145
|
+
// Apply per-request timeout
|
|
146
|
+
call.timeout().timeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
|
|
147
|
+
|
|
139
148
|
// Register the call if requestId is provided
|
|
140
149
|
config.requestId?.let { requestId ->
|
|
141
150
|
activeCalls[requestId] = call
|
|
@@ -191,14 +200,19 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
|
|
|
191
200
|
|
|
192
201
|
private fun getOrCreateClient(config: RequestConfig): OkHttpClient {
|
|
193
202
|
val normalizedHost = config.hostname.lowercase(Locale.US)
|
|
194
|
-
val key = ClientKey(
|
|
203
|
+
val key = ClientKey(normalizedHost, config.ip)
|
|
195
204
|
|
|
196
205
|
return clientCache.getOrPut(key) {
|
|
206
|
+
// Note: timeout is not part of the cache key to allow connection reuse
|
|
207
|
+
// Use reasonable default timeouts for the client
|
|
208
|
+
// Per-request timeout is applied via call.timeout() in performRequest
|
|
209
|
+
val defaultTimeout = 60_000L // 60 seconds
|
|
210
|
+
|
|
197
211
|
OkHttpClient.Builder()
|
|
198
|
-
.connectTimeout(
|
|
199
|
-
.readTimeout(
|
|
200
|
-
.writeTimeout(
|
|
201
|
-
.callTimeout(
|
|
212
|
+
.connectTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
|
|
213
|
+
.readTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
|
|
214
|
+
.writeTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
|
|
215
|
+
.callTimeout(0, TimeUnit.MILLISECONDS) // Disable client-level call timeout
|
|
202
216
|
.hostnameVerifier { _, session ->
|
|
203
217
|
HttpsURLConnection.getDefaultHostnameVerifier().verify(config.hostname, session)
|
|
204
218
|
}
|
|
@@ -179,8 +179,10 @@ final class SniConnectClient {
|
|
|
179
179
|
private static let cache = DNSCache()
|
|
180
180
|
|
|
181
181
|
/// LRU cache for DNS mappings with size limit and TTL support
|
|
182
|
+
/// Uses hostname:IP as cache key to ensure different IPs for the same hostname are isolated
|
|
182
183
|
private class DNSCache {
|
|
183
184
|
private struct CacheEntry {
|
|
185
|
+
let hostname: String
|
|
184
186
|
let ip: String
|
|
185
187
|
let timestamp: TimeInterval
|
|
186
188
|
}
|
|
@@ -188,33 +190,34 @@ final class SniConnectClient {
|
|
|
188
190
|
private var storage: [String: CacheEntry] = [:]
|
|
189
191
|
private var accessOrder: [String] = []
|
|
190
192
|
|
|
193
|
+
// Mapping from hostname to IP for DNS resolution
|
|
194
|
+
// This stores the most recently set IP for each hostname
|
|
195
|
+
private var hostnameToIP: [String: String] = [:]
|
|
196
|
+
|
|
191
197
|
// Maximum cache entries (100 as specified in requirements)
|
|
192
198
|
private let maxSize = 100
|
|
193
199
|
// TTL in seconds (5 minutes default)
|
|
194
200
|
private let ttl: TimeInterval = 300
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let now = Date().timeIntervalSince1970
|
|
202
|
-
if now - entry.timestamp > ttl {
|
|
203
|
-
remove(key)
|
|
204
|
-
return nil
|
|
205
|
-
}
|
|
202
|
+
/// Generate cache key using hostname:IP format
|
|
203
|
+
/// This ensures different IPs for the same hostname are isolated (accurate speed testing)
|
|
204
|
+
private func makeCacheKey(hostname: String, ip: String) -> String {
|
|
205
|
+
return "\(hostname.lowercased()):\(ip)"
|
|
206
|
+
}
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
accessOrder.remove(at: index)
|
|
210
|
-
}
|
|
211
|
-
accessOrder.append(key)
|
|
208
|
+
func get(_ domain: String) -> String? {
|
|
209
|
+
let normalizedDomain = domain.lowercased()
|
|
212
210
|
|
|
213
|
-
|
|
211
|
+
// Return the most recently set IP for this hostname
|
|
212
|
+
return hostnameToIP[normalizedDomain]
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
func set(_ ip: String, for domain: String) {
|
|
217
|
-
let
|
|
216
|
+
let normalizedDomain = domain.lowercased()
|
|
217
|
+
let key = makeCacheKey(hostname: normalizedDomain, ip: ip)
|
|
218
|
+
|
|
219
|
+
// Update hostname to IP mapping
|
|
220
|
+
hostnameToIP[normalizedDomain] = ip
|
|
218
221
|
|
|
219
222
|
// Evict oldest entry if cache is full
|
|
220
223
|
if storage.count >= maxSize && storage[key] == nil {
|
|
@@ -225,7 +228,7 @@ final class SniConnectClient {
|
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
// Add or update entry
|
|
228
|
-
let entry = CacheEntry(ip: ip, timestamp: Date().timeIntervalSince1970)
|
|
231
|
+
let entry = CacheEntry(hostname: normalizedDomain, ip: ip, timestamp: Date().timeIntervalSince1970)
|
|
229
232
|
storage[key] = entry
|
|
230
233
|
|
|
231
234
|
// Update access order
|
|
@@ -235,8 +238,7 @@ final class SniConnectClient {
|
|
|
235
238
|
accessOrder.append(key)
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
func remove(_
|
|
239
|
-
let key = domain.lowercased()
|
|
241
|
+
func remove(_ key: String) {
|
|
240
242
|
storage.removeValue(forKey: key)
|
|
241
243
|
if let index = accessOrder.firstIndex(of: key) {
|
|
242
244
|
accessOrder.remove(at: index)
|
|
@@ -246,6 +248,7 @@ final class SniConnectClient {
|
|
|
246
248
|
func clear() {
|
|
247
249
|
storage.removeAll()
|
|
248
250
|
accessOrder.removeAll()
|
|
251
|
+
hostnameToIP.removeAll()
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
func cleanExpired() {
|
|
@@ -253,6 +256,10 @@ final class SniConnectClient {
|
|
|
253
256
|
let expiredKeys = storage.filter { now - $0.value.timestamp > ttl }.map { $0.key }
|
|
254
257
|
for key in expiredKeys {
|
|
255
258
|
remove(key)
|
|
259
|
+
// Also remove from hostnameToIP if this was the active mapping
|
|
260
|
+
if let entry = storage[key], hostnameToIP[entry.hostname] == entry.ip {
|
|
261
|
+
hostnameToIP.removeValue(forKey: entry.hostname)
|
|
262
|
+
}
|
|
256
263
|
}
|
|
257
264
|
}
|
|
258
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-sni-connect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A React Native library for SNI-based HTTP requests with DNS caching and request management",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|