@onekeyfe/react-native-sni-connect 3.0.81 → 3.0.82
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/README.md +0 -7
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +61 -213
- package/android/src/main/java/com/sniconnect/SniConnectValidation.kt +76 -6
- package/android/src/main/java/com/sniconnect/SniPinnedTransport.kt +58 -0
- package/android/src/test/java/com/sniconnect/SniConnectValidationTest.kt +26 -26
- package/ios/SniConnect.mm +0 -19
- package/ios/SniConnect.swift +0 -29
- package/ios/SniConnectClient.swift +86 -75
- package/ios/SniConnectCore.swift +4 -0
- package/ios/SniConnectPinnedTransport.swift +92 -0
- package/ios/SniConnectValidation.swift +41 -214
- package/ios/Tests/SniConnectValidationTests/SniConnectValidationTests.swift +17 -206
- package/lib/module/index.js +0 -3
- package/lib/typescript/src/NativeSniConnect.d.ts +0 -13
- package/lib/typescript/src/index.d.ts +2 -3
- package/package.json +1 -1
- package/src/NativeSniConnect.ts +0 -17
- package/src/index.tsx +0 -10
- package/android/src/main/java/com/sniconnect/SniConnectRequestAdmission.kt +0 -276
- package/android/src/test/java/com/sniconnect/SniConnectRequestAdmissionTest.kt +0 -299
|
@@ -28,18 +28,6 @@ class SniConnectValidationTest {
|
|
|
28
28
|
assertEquals("/v1?q=1", SniConnectValidation.normalizePath("v1?q=1"))
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
@Test
|
|
32
|
-
fun canonicalizesEquivalentPublicIpLiterals() {
|
|
33
|
-
assertEquals(
|
|
34
|
-
"93.184.216.34",
|
|
35
|
-
SniConnectValidation.canonicalizePublicIp("093.184.216.034"),
|
|
36
|
-
)
|
|
37
|
-
assertEquals(
|
|
38
|
-
SniConnectValidation.canonicalizePublicIp("2001:4860:4860::8888"),
|
|
39
|
-
SniConnectValidation.canonicalizePublicIp("2001:4860:4860:0:0:0:0:8888"),
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
@Test
|
|
44
32
|
fun rejectsIpLiteralHostnames() {
|
|
45
33
|
assertValidationFails { SniConnectValidation.validateHostname("93.184.216.34") }
|
|
@@ -129,10 +117,8 @@ class SniConnectValidationTest {
|
|
|
129
117
|
|
|
130
118
|
@Test
|
|
131
119
|
fun enforcesRequestIdTimeoutAndBodyLimits() {
|
|
132
|
-
SniConnectValidation.validateRequestId("界".repeat(42))
|
|
133
120
|
assertValidationFails { SniConnectValidation.validateRequestId("") }
|
|
134
121
|
assertValidationFails { SniConnectValidation.validateRequestId("x".repeat(129)) }
|
|
135
|
-
assertValidationFails { SniConnectValidation.validateRequestId("界".repeat(43)) }
|
|
136
122
|
assertValidationFails { SniConnectValidation.validateRequestId("req\n1") }
|
|
137
123
|
assertValidationFails { SniConnectValidation.validateTimeout(0) }
|
|
138
124
|
assertValidationFails { SniConnectValidation.validateTimeout(120_001) }
|
|
@@ -198,6 +184,32 @@ class SniConnectValidationTest {
|
|
|
198
184
|
SniConnectValidation.validateMethodBody("OPTIONS", null)
|
|
199
185
|
}
|
|
200
186
|
|
|
187
|
+
@Test
|
|
188
|
+
fun requestLimiterEnforcesGlobalAndPerDestinationLimits() {
|
|
189
|
+
val limiter = SniConnectRequestLimiter(
|
|
190
|
+
maxActiveRequests = 2,
|
|
191
|
+
maxActiveRequestsPerPair = 1,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
val firstToken = limiter.acquire("Example.com", "93.184.216.34")
|
|
195
|
+
assertValidationFails {
|
|
196
|
+
limiter.acquire("example.com", "93.184.216.34")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
val secondToken = limiter.acquire("example.com", "93.184.216.35")
|
|
200
|
+
assertValidationFails {
|
|
201
|
+
limiter.acquire("example.net", "93.184.216.36")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
firstToken.release()
|
|
205
|
+
val replacementToken = limiter.acquire("example.com", "93.184.216.34")
|
|
206
|
+
firstToken.release()
|
|
207
|
+
secondToken.release()
|
|
208
|
+
replacementToken.release()
|
|
209
|
+
|
|
210
|
+
assertTrue(true)
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
@Test
|
|
202
214
|
fun classifiesSecurityFailuresAsFailClosedErrorCodes() {
|
|
203
215
|
assertEquals(
|
|
@@ -232,18 +244,6 @@ class SniConnectValidationTest {
|
|
|
232
244
|
"SNI_REQUEST_FAILED",
|
|
233
245
|
classifySniFailureCode(IOException("connection reset")),
|
|
234
246
|
)
|
|
235
|
-
assertEquals(
|
|
236
|
-
"SNI_CANCELLED",
|
|
237
|
-
classifySniResponseFailureCode(IOException("cancelled"), true),
|
|
238
|
-
)
|
|
239
|
-
assertEquals(
|
|
240
|
-
"SNI_REQUEST_TIMEOUT",
|
|
241
|
-
classifySniResponseFailureCode(SocketTimeoutException("timeout"), false),
|
|
242
|
-
)
|
|
243
|
-
assertEquals(
|
|
244
|
-
"SNI_RESPONSE_FAILED",
|
|
245
|
-
classifySniResponseFailureCode(IOException("bad body"), false),
|
|
246
|
-
)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
private fun assertValidationFails(block: () -> Unit) {
|
package/ios/SniConnect.mm
CHANGED
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
reject:(RCTPromiseRejectBlock)reject;
|
|
19
19
|
- (void)clearDNSCache:(RCTPromiseResolveBlock)resolve
|
|
20
20
|
reject:(RCTPromiseRejectBlock)reject;
|
|
21
|
-
- (void)getDebugSnapshot:(NSDictionary *)target
|
|
22
|
-
resolve:(RCTPromiseResolveBlock)resolve
|
|
23
|
-
reject:(RCTPromiseRejectBlock)reject;
|
|
24
21
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
25
22
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
26
23
|
reject:(RCTPromiseRejectBlock)reject;
|
|
@@ -92,16 +89,6 @@ RCT_EXPORT_MODULE(SniConnect)
|
|
|
92
89
|
[_implementation clearDNSCache:resolve reject:reject];
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
- (void)getDebugSnapshot:(JS::NativeSniConnect::SniConnectDebugTarget &)target
|
|
96
|
-
resolve:(RCTPromiseResolveBlock)resolve
|
|
97
|
-
reject:(RCTPromiseRejectBlock)reject {
|
|
98
|
-
NSDictionary *targetDict = @{
|
|
99
|
-
@"ip": target.ip() ?: @"",
|
|
100
|
-
@"hostname": target.hostname() ?: @"",
|
|
101
|
-
};
|
|
102
|
-
[_implementation getDebugSnapshot:targetDict resolve:resolve reject:reject];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
92
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
106
93
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
107
94
|
reject:(RCTPromiseRejectBlock)reject {
|
|
@@ -137,12 +124,6 @@ RCT_EXPORT_METHOD(clearDNSCache:(RCTPromiseResolveBlock)resolver
|
|
|
137
124
|
[_implementation clearDNSCache:resolver reject:rejecter];
|
|
138
125
|
}
|
|
139
126
|
|
|
140
|
-
RCT_EXPORT_METHOD(getDebugSnapshot:(NSDictionary *)target
|
|
141
|
-
resolver:(RCTPromiseResolveBlock)resolver
|
|
142
|
-
rejecter:(RCTPromiseRejectBlock)rejecter) {
|
|
143
|
-
[_implementation getDebugSnapshot:target resolve:resolver reject:rejecter];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
127
|
RCT_EXPORT_METHOD(isProxyActiveForUrl:(NSString *)url
|
|
147
128
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
148
129
|
rejecter:(RCTPromiseRejectBlock)rejecter) {
|
package/ios/SniConnect.swift
CHANGED
|
@@ -133,35 +133,6 @@ final class SniConnectImpl: NSObject {
|
|
|
133
133
|
resolve(["success": true])
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
@objc
|
|
137
|
-
public func getDebugSnapshot(
|
|
138
|
-
_ target: NSDictionary,
|
|
139
|
-
resolve: @escaping RCTPromiseResolveBlock,
|
|
140
|
-
reject: @escaping RCTPromiseRejectBlock
|
|
141
|
-
) {
|
|
142
|
-
do {
|
|
143
|
-
guard let ip = target["ip"] as? String, !ip.isEmpty else {
|
|
144
|
-
throw SniConnectError.invalidConfig("Missing ip")
|
|
145
|
-
}
|
|
146
|
-
guard let hostname = target["hostname"] as? String, !hostname.isEmpty else {
|
|
147
|
-
throw SniConnectError.invalidConfig("Missing hostname")
|
|
148
|
-
}
|
|
149
|
-
try SniConnectValidation.validatePublicIP(ip)
|
|
150
|
-
try SniConnectValidation.validateHostname(hostname)
|
|
151
|
-
let snapshot = client.debugSnapshot(hostname: hostname, ip: ip)
|
|
152
|
-
resolve([
|
|
153
|
-
"activeRequests": snapshot.activeRequests,
|
|
154
|
-
"activeRequestsForPair": snapshot.activeRequestsForPair,
|
|
155
|
-
"pendingRequests": snapshot.pendingRequests,
|
|
156
|
-
"pendingRequestsForPair": snapshot.pendingRequestsForPair,
|
|
157
|
-
"activeRequestIdsForPair": snapshot.activeRequestIdsForPair,
|
|
158
|
-
"pendingRequestIdsForPair": snapshot.pendingRequestIdsForPair,
|
|
159
|
-
])
|
|
160
|
-
} catch {
|
|
161
|
-
reject("SNI_INVALID_CONFIG", "\(error)", error)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
136
|
@objc
|
|
166
137
|
public func isProxyActiveForUrl(
|
|
167
138
|
_ url: String,
|
|
@@ -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,15 +90,22 @@ private final class SniConnectPinnedResolverLease {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionDataDelegate {
|
|
94
94
|
private let hostname: String
|
|
95
95
|
private let ip: String
|
|
96
96
|
private let resolverLease: SniConnectPinnedResolverLease
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
private weak var forwardingDataDelegate: URLSessionDataDelegate?
|
|
98
|
+
|
|
99
|
+
init(
|
|
100
|
+
hostname: String,
|
|
101
|
+
ip: String,
|
|
102
|
+
resolverLease: SniConnectPinnedResolverLease,
|
|
103
|
+
forwardingDataDelegate: URLSessionDataDelegate? = nil
|
|
104
|
+
) {
|
|
99
105
|
self.hostname = hostname
|
|
100
106
|
self.ip = ip
|
|
101
107
|
self.resolverLease = resolverLease
|
|
108
|
+
self.forwardingDataDelegate = forwardingDataDelegate
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
|
|
@@ -110,6 +117,66 @@ private final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionD
|
|
|
110
117
|
("success", error == nil),
|
|
111
118
|
]))
|
|
112
119
|
}
|
|
120
|
+
|
|
121
|
+
func urlSession(
|
|
122
|
+
_ session: URLSession,
|
|
123
|
+
task: URLSessionTask,
|
|
124
|
+
willPerformHTTPRedirection response: HTTPURLResponse,
|
|
125
|
+
newRequest request: URLRequest,
|
|
126
|
+
completionHandler: @escaping (URLRequest?) -> Void
|
|
127
|
+
) {
|
|
128
|
+
if let forwardingDataDelegate {
|
|
129
|
+
forwardingDataDelegate.urlSession?(
|
|
130
|
+
session,
|
|
131
|
+
task: task,
|
|
132
|
+
willPerformHTTPRedirection: response,
|
|
133
|
+
newRequest: request,
|
|
134
|
+
completionHandler: completionHandler
|
|
135
|
+
)
|
|
136
|
+
} else {
|
|
137
|
+
completionHandler(nil)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func urlSession(
|
|
142
|
+
_ session: URLSession,
|
|
143
|
+
dataTask: URLSessionDataTask,
|
|
144
|
+
didReceive response: URLResponse,
|
|
145
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
146
|
+
) {
|
|
147
|
+
forwardingDataDelegate?.urlSession?(
|
|
148
|
+
session,
|
|
149
|
+
dataTask: dataTask,
|
|
150
|
+
didReceive: response,
|
|
151
|
+
completionHandler: completionHandler
|
|
152
|
+
) ?? completionHandler(
|
|
153
|
+
SniConnectSessionDelegatePolicy.responseDispositionWithoutForwardingDelegate
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func urlSession(
|
|
158
|
+
_ session: URLSession,
|
|
159
|
+
dataTask: URLSessionDataTask,
|
|
160
|
+
didReceive data: Data
|
|
161
|
+
) {
|
|
162
|
+
forwardingDataDelegate?.urlSession?(
|
|
163
|
+
session,
|
|
164
|
+
dataTask: dataTask,
|
|
165
|
+
didReceive: data
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func urlSession(
|
|
170
|
+
_ session: URLSession,
|
|
171
|
+
task: URLSessionTask,
|
|
172
|
+
didCompleteWithError error: Error?
|
|
173
|
+
) {
|
|
174
|
+
forwardingDataDelegate?.urlSession?(
|
|
175
|
+
session,
|
|
176
|
+
task: task,
|
|
177
|
+
didCompleteWithError: error
|
|
178
|
+
)
|
|
179
|
+
}
|
|
113
180
|
}
|
|
114
181
|
|
|
115
182
|
/// Core HTTPS client that enforces IP direct connection with SNI.
|
|
@@ -120,7 +187,7 @@ final class SniConnectClient {
|
|
|
120
187
|
private var activeTasksByToken: [UUID: Task<Response, Error>] = [:]
|
|
121
188
|
private var requestTokensById: [String: UUID] = [:]
|
|
122
189
|
private let tasksQueue = DispatchQueue(label: "com.onekey.sni.connect.tasks", attributes: .concurrent)
|
|
123
|
-
private let requestLimiter = SniConnectRequestLimiter
|
|
190
|
+
private let requestLimiter = SniConnectRequestLimiter()
|
|
124
191
|
|
|
125
192
|
private struct SessionKey: Hashable {
|
|
126
193
|
let hostname: String
|
|
@@ -334,33 +401,10 @@ final class SniConnectClient {
|
|
|
334
401
|
}
|
|
335
402
|
|
|
336
403
|
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(
|
|
404
|
+
let resources = try SniConnectPinnedTransport.makeResources(
|
|
359
405
|
hostname: key.hostname,
|
|
360
406
|
ip: key.ip
|
|
361
407
|
)
|
|
362
|
-
|
|
363
|
-
EMASCurlProtocol.install(into: configuration, with: curlConfig)
|
|
364
408
|
SniConnectLog.info(SniConnectLog.event("sni_transport_config", [
|
|
365
409
|
("hostname", key.hostname),
|
|
366
410
|
("ipHash", SniConnectLog.shortHash(key.ip)),
|
|
@@ -371,15 +415,13 @@ final class SniConnectClient {
|
|
|
371
415
|
("followRedirects", false),
|
|
372
416
|
("cacheEnabled", false),
|
|
373
417
|
]))
|
|
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
418
|
return ManagedSession(
|
|
381
|
-
session: URLSession(
|
|
382
|
-
|
|
419
|
+
session: URLSession(
|
|
420
|
+
configuration: resources.configuration,
|
|
421
|
+
delegate: resources.delegate,
|
|
422
|
+
delegateQueue: nil
|
|
423
|
+
),
|
|
424
|
+
resolverLease: resources.resolverLease
|
|
383
425
|
)
|
|
384
426
|
}
|
|
385
427
|
|
|
@@ -510,10 +552,6 @@ final class SniConnectClient {
|
|
|
510
552
|
]))
|
|
511
553
|
}
|
|
512
554
|
|
|
513
|
-
func debugSnapshot(hostname: String, ip: String) -> SniConnectRequestLimiter.Snapshot {
|
|
514
|
-
requestLimiter.snapshot(hostname: hostname, ip: ip)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
555
|
/// Cancel a request by ID
|
|
518
556
|
func cancelRequest(requestId: String) -> Bool {
|
|
519
557
|
return tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
@@ -620,24 +658,9 @@ final class SniConnectClient {
|
|
|
620
658
|
|
|
621
659
|
let requestSlot: SniConnectRequestLimiter.Token
|
|
622
660
|
do {
|
|
623
|
-
|
|
624
|
-
let limiter = requestLimiter
|
|
625
|
-
let admissionTimeoutMilliseconds =
|
|
626
|
-
config.effectiveTotalTimeout - Date().timeIntervalSince(startedAt) * 1_000.0
|
|
627
|
-
guard admissionTimeoutMilliseconds > 0 else {
|
|
628
|
-
throw SniConnectTimeout.deadlineExceeded
|
|
629
|
-
}
|
|
630
|
-
requestSlot = try await SniConnectWallClockDeadline.run(
|
|
631
|
-
timeoutMilliseconds: admissionTimeoutMilliseconds
|
|
632
|
-
) {
|
|
633
|
-
try await limiter.acquire(
|
|
634
|
-
hostname: config.hostname,
|
|
635
|
-
ip: config.ip,
|
|
636
|
-
requestId: config.requestId
|
|
637
|
-
)
|
|
638
|
-
}
|
|
661
|
+
requestSlot = try requestLimiter.acquire(hostname: config.hostname, ip: config.ip)
|
|
639
662
|
} catch {
|
|
640
|
-
let sniError = SniConnectError.
|
|
663
|
+
let sniError = SniConnectError.resourceLimit("\(error)")
|
|
641
664
|
SniConnectLog.error(SniConnectLog.event("sni_request_result", [
|
|
642
665
|
("result", "error"),
|
|
643
666
|
("code", sniError.code),
|
|
@@ -655,14 +678,6 @@ final class SniConnectClient {
|
|
|
655
678
|
defer {
|
|
656
679
|
requestSlot.release()
|
|
657
680
|
}
|
|
658
|
-
try Task.checkCancellation()
|
|
659
|
-
|
|
660
|
-
let queueWaitMilliseconds = SniConnectLog.elapsedMs(since: startedAt)
|
|
661
|
-
let remainingTimeoutMilliseconds =
|
|
662
|
-
config.effectiveTotalTimeout - Double(queueWaitMilliseconds)
|
|
663
|
-
guard remainingTimeoutMilliseconds > 0 else {
|
|
664
|
-
throw SniConnectError.requestTimeout
|
|
665
|
-
}
|
|
666
681
|
|
|
667
682
|
let url = try Self.buildURL(hostname: config.hostname, normalizedPath: normalizedPath)
|
|
668
683
|
|
|
@@ -670,9 +685,8 @@ final class SniConnectClient {
|
|
|
670
685
|
mutableRequest.httpMethod = method
|
|
671
686
|
|
|
672
687
|
// Convert milliseconds to seconds for timeout values
|
|
673
|
-
let totalTimeoutSeconds =
|
|
674
|
-
let connectTimeoutSeconds =
|
|
675
|
-
min(config.effectiveConnectTimeout, remainingTimeoutMilliseconds) / 1000.0
|
|
688
|
+
let totalTimeoutSeconds = config.effectiveTotalTimeout / 1000.0
|
|
689
|
+
let connectTimeoutSeconds = config.effectiveConnectTimeout / 1000.0
|
|
676
690
|
|
|
677
691
|
// URLRequest.timeoutInterval is not a full request deadline. Keep it aligned
|
|
678
692
|
// with the caller timeout as a transport guard; the wall-clock deadline below
|
|
@@ -702,15 +716,12 @@ final class SniConnectClient {
|
|
|
702
716
|
("method", method),
|
|
703
717
|
("timeoutMs", Int(config.effectiveTotalTimeout)),
|
|
704
718
|
("connectTimeoutMs", Int(config.effectiveConnectTimeout)),
|
|
705
|
-
("queueWaitMs", queueWaitMilliseconds),
|
|
706
719
|
("headerCount", normalizedHeaders.count),
|
|
707
720
|
("bodyBytes", config.body?.data(using: .utf8)?.count ?? 0),
|
|
708
721
|
]))
|
|
709
722
|
|
|
710
723
|
do {
|
|
711
|
-
return try await SniConnectWallClockDeadline.run(
|
|
712
|
-
timeoutMilliseconds: remainingTimeoutMilliseconds
|
|
713
|
-
) {
|
|
724
|
+
return try await SniConnectWallClockDeadline.run(timeoutMilliseconds: config.effectiveTotalTimeout) {
|
|
714
725
|
let lease = try await self.sessionLease(for: config)
|
|
715
726
|
defer {
|
|
716
727
|
self.releaseSessionLease(lease)
|
package/ios/SniConnectCore.swift
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
dataDelegate: URLSessionDataDelegate? = nil
|
|
35
|
+
) throws -> SniConnectPinnedTransportResources {
|
|
36
|
+
try SniConnectValidation.validateHostname(hostname)
|
|
37
|
+
try SniConnectValidation.validatePublicIP(ip)
|
|
38
|
+
|
|
39
|
+
let normalizedHostname = hostname.lowercased()
|
|
40
|
+
let configuration = URLSessionConfiguration.default
|
|
41
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
42
|
+
configuration.urlCache = nil
|
|
43
|
+
configuration.httpCookieStorage = nil
|
|
44
|
+
configuration.httpShouldSetCookies = false
|
|
45
|
+
configuration.connectionProxyDictionary = [:]
|
|
46
|
+
configuration.shouldUseExtendedBackgroundIdleMode = false
|
|
47
|
+
|
|
48
|
+
let curlConfig = EMASCurlConfiguration.default()
|
|
49
|
+
curlConfig.httpVersion = .HTTP1
|
|
50
|
+
curlConfig.connectTimeoutInterval = 2.5
|
|
51
|
+
curlConfig.enableBuiltInGzip = false
|
|
52
|
+
curlConfig.enableBuiltInRedirection = false
|
|
53
|
+
curlConfig.cacheEnabled = false
|
|
54
|
+
curlConfig.certificateValidationEnabled = true
|
|
55
|
+
curlConfig.domainNameVerificationEnabled = true
|
|
56
|
+
curlConfig.dnsResolver = try PinnedDNSResolverFactory.resolverClass(
|
|
57
|
+
hostname: normalizedHostname,
|
|
58
|
+
ip: ip
|
|
59
|
+
)
|
|
60
|
+
EMASCurlProtocol.install(into: configuration, with: curlConfig)
|
|
61
|
+
|
|
62
|
+
let resolverLease = SniConnectPinnedResolverLease(
|
|
63
|
+
hostname: normalizedHostname,
|
|
64
|
+
ip: ip
|
|
65
|
+
)
|
|
66
|
+
let delegate = SniConnectSessionInvalidationDelegate(
|
|
67
|
+
hostname: normalizedHostname,
|
|
68
|
+
ip: ip,
|
|
69
|
+
resolverLease: resolverLease,
|
|
70
|
+
forwardingDataDelegate: dataDelegate
|
|
71
|
+
)
|
|
72
|
+
return SniConnectPinnedTransportResources(
|
|
73
|
+
configuration: configuration,
|
|
74
|
+
resolverLease: resolverLease,
|
|
75
|
+
delegate: delegate
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public static func makeSession(
|
|
80
|
+
hostname: String,
|
|
81
|
+
ip: String,
|
|
82
|
+
dataDelegate: URLSessionDataDelegate? = nil
|
|
83
|
+
) throws -> SniConnectPinnedSession {
|
|
84
|
+
SniConnectPinnedSession(
|
|
85
|
+
resources: try makeResources(
|
|
86
|
+
hostname: hostname,
|
|
87
|
+
ip: ip,
|
|
88
|
+
dataDelegate: dataDelegate
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|