@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 =
|
|
409
|
-
.
|
|
410
|
-
.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
382
|
-
|
|
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.
|
|
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
|
}
|