@onekeyfe/react-native-sni-connect 3.0.80 → 3.0.81-alpha.1

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.
@@ -13,11 +13,9 @@ import okhttp3.Call
13
13
  import okhttp3.Callback
14
14
  import okhttp3.ConnectionPool
15
15
  import okhttp3.Dispatcher
16
- import okhttp3.Dns
17
16
  import okhttp3.Headers
18
17
  import okhttp3.MediaType.Companion.toMediaTypeOrNull
19
18
  import okhttp3.OkHttpClient
20
- import okhttp3.Protocol
21
19
  import okhttp3.Request
22
20
  import okhttp3.RequestBody.Companion.toRequestBody
23
21
  import okhttp3.Response
@@ -25,7 +23,6 @@ import okhttp3.ResponseBody
25
23
  import okio.Buffer
26
24
  import java.io.IOException
27
25
  import java.io.InterruptedIOException
28
- import java.net.InetAddress
29
26
  import java.net.Proxy
30
27
  import java.net.ProxySelector
31
28
  import java.net.URI
@@ -37,7 +34,6 @@ import java.util.Locale
37
34
  import java.util.concurrent.ConcurrentHashMap
38
35
  import java.util.concurrent.TimeUnit
39
36
  import java.util.concurrent.atomic.AtomicBoolean
40
- import javax.net.ssl.HttpsURLConnection
41
37
  import javax.net.ssl.SSLException
42
38
  import javax.net.ssl.SSLPeerUnverifiedException
43
39
 
@@ -405,24 +401,12 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
405
401
  synchronized(clientCache) {
406
402
  clientCache[key]?.let { return it }
407
403
 
408
- val client = OkHttpClient.Builder()
409
- .dispatcher(sharedDispatcher)
410
- .connectionPool(sharedConnectionPool)
411
- .proxy(Proxy.NO_PROXY)
412
- .protocols(listOf(Protocol.HTTP_1_1))
413
- .connectTimeout(0, TimeUnit.MILLISECONDS)
414
- .readTimeout(0, TimeUnit.MILLISECONDS)
415
- .writeTimeout(0, TimeUnit.MILLISECONDS)
416
- .callTimeout(0, TimeUnit.MILLISECONDS)
417
- .followRedirects(false)
418
- .followSslRedirects(false)
419
- // TLS is validated normally: cert chain via the default trust manager and
420
- // hostname verification against the REAL hostname (not the pinned IP).
421
- .hostnameVerifier { _, session ->
422
- HttpsURLConnection.getDefaultHostnameVerifier().verify(config.hostname, session)
423
- }
424
- .dns(createPinnedDns(config.ip, config.hostname))
425
- .build()
404
+ val client = SniPinnedTransport.createClient(
405
+ ip = config.ip,
406
+ hostname = config.hostname,
407
+ dispatcher = sharedDispatcher,
408
+ connectionPool = sharedConnectionPool,
409
+ )
426
410
 
427
411
  clientCache[key] = client
428
412
  SniConnectLogger.info(
@@ -442,29 +426,6 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
442
426
  }
443
427
  }
444
428
 
445
- private fun createPinnedDns(ip: String, hostname: String): Dns =
446
- object : Dns {
447
- private val expectedHost = hostname.lowercase(Locale.US)
448
- // Resolve the literal IP once up front (validated; never triggers DNS).
449
- private val pinnedAddress: InetAddress = SniConnectValidation.literalToInetAddress(ip)
450
-
451
- override fun lookup(requestedHost: String): List<InetAddress> {
452
- return if (requestedHost.lowercase(Locale.US) == expectedHost) {
453
- listOf(pinnedAddress)
454
- } else {
455
- SniConnectLogger.warn(
456
- SniConnectLogger.event(
457
- "sni_pinned_dns_unexpected_host",
458
- "expectedHost" to expectedHost,
459
- "requestedHostHash" to SniConnectLogger.shortHash(requestedHost.lowercase(Locale.US)),
460
- "result" to "fail_closed",
461
- ),
462
- )
463
- throw UnknownHostException("Unexpected host for pinned SNI request: $requestedHost")
464
- }
465
- }
466
- }
467
-
468
429
  /**
469
430
  * Build the request. Always `https://<hostname><path>` on the implicit port 443 —
470
431
  * `path` has been validated as relative, so scheme/host/port cannot be overridden.
@@ -0,0 +1,58 @@
1
+ package com.sniconnect
2
+
3
+ import java.net.InetAddress
4
+ import java.net.Proxy
5
+ import java.net.UnknownHostException
6
+ import java.util.Locale
7
+ import java.util.concurrent.TimeUnit
8
+ import okhttp3.ConnectionPool
9
+ import okhttp3.Dispatcher
10
+ import okhttp3.Dns
11
+ import okhttp3.OkHttpClient
12
+ import okhttp3.Protocol
13
+ import javax.net.ssl.HttpsURLConnection
14
+
15
+ object SniPinnedTransport {
16
+ @JvmStatic
17
+ fun createClient(
18
+ ip: String,
19
+ hostname: String,
20
+ dispatcher: Dispatcher = Dispatcher(),
21
+ connectionPool: ConnectionPool = ConnectionPool(),
22
+ ): OkHttpClient {
23
+ SniConnectValidation.validatePublicIp(ip)
24
+ SniConnectValidation.validateHostname(hostname)
25
+ val normalizedHostname = hostname.lowercase(Locale.US)
26
+ return OkHttpClient.Builder()
27
+ .dispatcher(dispatcher)
28
+ .connectionPool(connectionPool)
29
+ .proxy(Proxy.NO_PROXY)
30
+ .protocols(listOf(Protocol.HTTP_1_1))
31
+ .connectTimeout(0, TimeUnit.MILLISECONDS)
32
+ .readTimeout(0, TimeUnit.MILLISECONDS)
33
+ .writeTimeout(0, TimeUnit.MILLISECONDS)
34
+ .callTimeout(0, TimeUnit.MILLISECONDS)
35
+ .followRedirects(false)
36
+ .followSslRedirects(false)
37
+ .hostnameVerifier { _, session ->
38
+ HttpsURLConnection.getDefaultHostnameVerifier().verify(normalizedHostname, session)
39
+ }
40
+ .dns(createPinnedDns(ip, normalizedHostname))
41
+ .build()
42
+ }
43
+
44
+ private fun createPinnedDns(ip: String, hostname: String): Dns =
45
+ object : Dns {
46
+ private val pinnedAddress: InetAddress =
47
+ SniConnectValidation.literalToInetAddress(ip)
48
+
49
+ override fun lookup(requestedHost: String): List<InetAddress> {
50
+ if (requestedHost.lowercase(Locale.US) == hostname) {
51
+ return listOf(pinnedAddress)
52
+ }
53
+ throw UnknownHostException(
54
+ "Unexpected host for pinned SNI request: $requestedHost"
55
+ )
56
+ }
57
+ }
58
+ }
@@ -4,13 +4,13 @@ import UIKit
4
4
  import EMASCurl
5
5
 
6
6
  @objc(SniConnectPinnedDNSResolverBase)
7
- private class SniConnectPinnedDNSResolverBase: NSObject, EMASCurlProtocolDNSResolver {
7
+ class SniConnectPinnedDNSResolverBase: NSObject, EMASCurlProtocolDNSResolver {
8
8
  @objc class func resolveDomain(_ domain: String) -> String? {
9
9
  PinnedDNSResolverFactory.resolve(domain: domain, resolverClass: self)
10
10
  }
11
11
  }
12
12
 
13
- private enum PinnedDNSResolverFactory {
13
+ enum PinnedDNSResolverFactory {
14
14
  private static let queue = DispatchQueue(label: "com.onekey.sni.connect.pinned-dns-resolvers")
15
15
  private static var nextClassID = 0
16
16
  private static let registry = SniConnectPinnedResolverRegistry()
@@ -54,7 +54,7 @@ private enum PinnedDNSResolverFactory {
54
54
  }
55
55
  }
56
56
 
57
- private final class SniConnectPinnedResolverLease {
57
+ final class SniConnectPinnedResolverLease {
58
58
  private let hostname: String
59
59
  private let ip: String
60
60
  private let queue = DispatchQueue(label: "com.onekey.sni.connect.resolver-lease")
@@ -90,7 +90,7 @@ private final class SniConnectPinnedResolverLease {
90
90
  }
91
91
  }
92
92
 
93
- private final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionDelegate {
93
+ final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionDelegate {
94
94
  private let hostname: String
95
95
  private let ip: String
96
96
  private let resolverLease: SniConnectPinnedResolverLease
@@ -334,33 +334,10 @@ final class SniConnectClient {
334
334
  }
335
335
 
336
336
  private static func makeURLSession(for key: SessionKey) throws -> ManagedSession {
337
- let configuration = URLSessionConfiguration.default
338
- configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
339
- configuration.urlCache = nil
340
- configuration.httpCookieStorage = nil
341
- configuration.httpShouldSetCookies = false
342
- configuration.connectionProxyDictionary = [:]
343
- configuration.shouldUseExtendedBackgroundIdleMode = false
344
-
345
- let curlConfig = EMASCurlConfiguration.default()
346
- curlConfig.httpVersion = .HTTP1
347
- curlConfig.connectTimeoutInterval = 2.5
348
- curlConfig.enableBuiltInGzip = false
349
- curlConfig.enableBuiltInRedirection = false
350
- curlConfig.cacheEnabled = false
351
-
352
- // Enable full certificate validation for security.
353
- // The certificate is validated against the SNI hostname, not the IP, because
354
- // the custom DNS resolver only overrides address resolution — libcurl keeps the
355
- // original hostname for SNI and certificate CN/SAN matching.
356
- curlConfig.certificateValidationEnabled = true
357
- curlConfig.domainNameVerificationEnabled = true
358
- curlConfig.dnsResolver = try PinnedDNSResolverFactory.resolverClass(
337
+ let resources = try SniConnectPinnedTransport.makeResources(
359
338
  hostname: key.hostname,
360
339
  ip: key.ip
361
340
  )
362
-
363
- EMASCurlProtocol.install(into: configuration, with: curlConfig)
364
341
  SniConnectLog.info(SniConnectLog.event("sni_transport_config", [
365
342
  ("hostname", key.hostname),
366
343
  ("ipHash", SniConnectLog.shortHash(key.ip)),
@@ -371,15 +348,13 @@ final class SniConnectClient {
371
348
  ("followRedirects", false),
372
349
  ("cacheEnabled", false),
373
350
  ]))
374
- let resolverLease = SniConnectPinnedResolverLease(hostname: key.hostname, ip: key.ip)
375
- let delegate = SniConnectSessionInvalidationDelegate(
376
- hostname: key.hostname,
377
- ip: key.ip,
378
- resolverLease: resolverLease
379
- )
380
351
  return ManagedSession(
381
- session: URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil),
382
- resolverLease: resolverLease
352
+ session: URLSession(
353
+ configuration: resources.configuration,
354
+ delegate: resources.delegate,
355
+ delegateQueue: nil
356
+ ),
357
+ resolverLease: resources.resolverLease
383
358
  )
384
359
  }
385
360
 
@@ -0,0 +1,85 @@
1
+ import Foundation
2
+ import EMASCurl
3
+
4
+ struct SniConnectPinnedTransportResources {
5
+ let configuration: URLSessionConfiguration
6
+ let resolverLease: SniConnectPinnedResolverLease
7
+ let delegate: SniConnectSessionInvalidationDelegate
8
+ }
9
+
10
+ public final class SniConnectPinnedSession {
11
+ public let session: URLSession
12
+
13
+ init(resources: SniConnectPinnedTransportResources) {
14
+ session = URLSession(
15
+ configuration: resources.configuration,
16
+ delegate: resources.delegate,
17
+ delegateQueue: nil
18
+ )
19
+ }
20
+
21
+ public func close() {
22
+ session.finishTasksAndInvalidate()
23
+ }
24
+
25
+ deinit {
26
+ close()
27
+ }
28
+ }
29
+
30
+ public enum SniConnectPinnedTransport {
31
+ static func makeResources(
32
+ hostname: String,
33
+ ip: String
34
+ ) throws -> SniConnectPinnedTransportResources {
35
+ try SniConnectValidation.validateHostname(hostname)
36
+ try SniConnectValidation.validatePublicIP(ip)
37
+
38
+ let normalizedHostname = hostname.lowercased()
39
+ let configuration = URLSessionConfiguration.default
40
+ configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
41
+ configuration.urlCache = nil
42
+ configuration.httpCookieStorage = nil
43
+ configuration.httpShouldSetCookies = false
44
+ configuration.connectionProxyDictionary = [:]
45
+ configuration.shouldUseExtendedBackgroundIdleMode = false
46
+
47
+ let curlConfig = EMASCurlConfiguration.default()
48
+ curlConfig.httpVersion = .HTTP1
49
+ curlConfig.connectTimeoutInterval = 2.5
50
+ curlConfig.enableBuiltInGzip = false
51
+ curlConfig.enableBuiltInRedirection = false
52
+ curlConfig.cacheEnabled = false
53
+ curlConfig.certificateValidationEnabled = true
54
+ curlConfig.domainNameVerificationEnabled = true
55
+ curlConfig.dnsResolver = try PinnedDNSResolverFactory.resolverClass(
56
+ hostname: normalizedHostname,
57
+ ip: ip
58
+ )
59
+ EMASCurlProtocol.install(into: configuration, with: curlConfig)
60
+
61
+ let resolverLease = SniConnectPinnedResolverLease(
62
+ hostname: normalizedHostname,
63
+ ip: ip
64
+ )
65
+ let delegate = SniConnectSessionInvalidationDelegate(
66
+ hostname: normalizedHostname,
67
+ ip: ip,
68
+ resolverLease: resolverLease
69
+ )
70
+ return SniConnectPinnedTransportResources(
71
+ configuration: configuration,
72
+ resolverLease: resolverLease,
73
+ delegate: delegate
74
+ )
75
+ }
76
+
77
+ public static func makeSession(
78
+ hostname: String,
79
+ ip: String
80
+ ) throws -> SniConnectPinnedSession {
81
+ SniConnectPinnedSession(
82
+ resources: try makeResources(hostname: hostname, ip: ip)
83
+ )
84
+ }
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-sni-connect",
3
- "version": "3.0.80",
3
+ "version": "3.0.81-alpha.1",
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",
@@ -161,5 +161,6 @@
161
161
  "languages": "kotlin-objc",
162
162
  "type": "turbo-module",
163
163
  "version": "0.54.8"
164
- }
164
+ },
165
+ "stableVersion": "3.0.80"
165
166
  }