@onekeyfe/react-native-sni-connect 3.0.81-alpha.7 → 3.0.81
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 +7 -0
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +213 -61
- package/android/src/main/java/com/sniconnect/SniConnectRequestAdmission.kt +276 -0
- package/android/src/main/java/com/sniconnect/SniConnectValidation.kt +6 -76
- package/android/src/test/java/com/sniconnect/SniConnectRequestAdmissionTest.kt +299 -0
- package/android/src/test/java/com/sniconnect/SniConnectValidationTest.kt +26 -26
- package/ios/SniConnect.mm +19 -0
- package/ios/SniConnect.swift +29 -0
- package/ios/SniConnectClient.swift +75 -84
- package/ios/SniConnectValidation.swift +214 -41
- package/ios/Tests/SniConnectValidationTests/SniConnectValidationTests.swift +206 -8
- package/lib/module/index.js +3 -0
- package/lib/typescript/src/NativeSniConnect.d.ts +13 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/package.json +2 -3
- package/src/NativeSniConnect.ts +17 -0
- package/src/index.tsx +10 -0
- package/android/src/main/java/com/sniconnect/SniPinnedTransport.kt +0 -58
- package/ios/SniConnectPinnedTransport.swift +0 -92
|
@@ -28,6 +28,18 @@ 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
|
+
|
|
31
43
|
@Test
|
|
32
44
|
fun rejectsIpLiteralHostnames() {
|
|
33
45
|
assertValidationFails { SniConnectValidation.validateHostname("93.184.216.34") }
|
|
@@ -117,8 +129,10 @@ class SniConnectValidationTest {
|
|
|
117
129
|
|
|
118
130
|
@Test
|
|
119
131
|
fun enforcesRequestIdTimeoutAndBodyLimits() {
|
|
132
|
+
SniConnectValidation.validateRequestId("界".repeat(42))
|
|
120
133
|
assertValidationFails { SniConnectValidation.validateRequestId("") }
|
|
121
134
|
assertValidationFails { SniConnectValidation.validateRequestId("x".repeat(129)) }
|
|
135
|
+
assertValidationFails { SniConnectValidation.validateRequestId("界".repeat(43)) }
|
|
122
136
|
assertValidationFails { SniConnectValidation.validateRequestId("req\n1") }
|
|
123
137
|
assertValidationFails { SniConnectValidation.validateTimeout(0) }
|
|
124
138
|
assertValidationFails { SniConnectValidation.validateTimeout(120_001) }
|
|
@@ -184,32 +198,6 @@ class SniConnectValidationTest {
|
|
|
184
198
|
SniConnectValidation.validateMethodBody("OPTIONS", null)
|
|
185
199
|
}
|
|
186
200
|
|
|
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
|
-
|
|
213
201
|
@Test
|
|
214
202
|
fun classifiesSecurityFailuresAsFailClosedErrorCodes() {
|
|
215
203
|
assertEquals(
|
|
@@ -244,6 +232,18 @@ class SniConnectValidationTest {
|
|
|
244
232
|
"SNI_REQUEST_FAILED",
|
|
245
233
|
classifySniFailureCode(IOException("connection reset")),
|
|
246
234
|
)
|
|
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,6 +18,9 @@
|
|
|
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;
|
|
21
24
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
22
25
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
23
26
|
reject:(RCTPromiseRejectBlock)reject;
|
|
@@ -89,6 +92,16 @@ RCT_EXPORT_MODULE(SniConnect)
|
|
|
89
92
|
[_implementation clearDNSCache:resolve reject:reject];
|
|
90
93
|
}
|
|
91
94
|
|
|
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
|
+
|
|
92
105
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
93
106
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
94
107
|
reject:(RCTPromiseRejectBlock)reject {
|
|
@@ -124,6 +137,12 @@ RCT_EXPORT_METHOD(clearDNSCache:(RCTPromiseResolveBlock)resolver
|
|
|
124
137
|
[_implementation clearDNSCache:resolver reject:rejecter];
|
|
125
138
|
}
|
|
126
139
|
|
|
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
|
+
|
|
127
146
|
RCT_EXPORT_METHOD(isProxyActiveForUrl:(NSString *)url
|
|
128
147
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
129
148
|
rejecter:(RCTPromiseRejectBlock)rejecter) {
|
package/ios/SniConnect.swift
CHANGED
|
@@ -133,6 +133,35 @@ 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
|
+
|
|
136
165
|
@objc
|
|
137
166
|
public func isProxyActiveForUrl(
|
|
138
167
|
_ url: String,
|
|
@@ -4,13 +4,13 @@ import UIKit
|
|
|
4
4
|
import EMASCurl
|
|
5
5
|
|
|
6
6
|
@objc(SniConnectPinnedDNSResolverBase)
|
|
7
|
-
class SniConnectPinnedDNSResolverBase: NSObject, EMASCurlProtocolDNSResolver {
|
|
7
|
+
private 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
|
-
enum PinnedDNSResolverFactory {
|
|
13
|
+
private 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 @@ enum PinnedDNSResolverFactory {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
final class SniConnectPinnedResolverLease {
|
|
57
|
+
private 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,22 +90,15 @@ final class SniConnectPinnedResolverLease {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
final class SniConnectSessionInvalidationDelegate: NSObject,
|
|
93
|
+
private final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionDelegate {
|
|
94
94
|
private let hostname: String
|
|
95
95
|
private let ip: String
|
|
96
96
|
private let resolverLease: SniConnectPinnedResolverLease
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
init(
|
|
100
|
-
hostname: String,
|
|
101
|
-
ip: String,
|
|
102
|
-
resolverLease: SniConnectPinnedResolverLease,
|
|
103
|
-
forwardingDataDelegate: URLSessionDataDelegate? = nil
|
|
104
|
-
) {
|
|
97
|
+
|
|
98
|
+
init(hostname: String, ip: String, resolverLease: SniConnectPinnedResolverLease) {
|
|
105
99
|
self.hostname = hostname
|
|
106
100
|
self.ip = ip
|
|
107
101
|
self.resolverLease = resolverLease
|
|
108
|
-
self.forwardingDataDelegate = forwardingDataDelegate
|
|
109
102
|
}
|
|
110
103
|
|
|
111
104
|
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
|
|
@@ -117,64 +110,6 @@ final class SniConnectSessionInvalidationDelegate: NSObject, URLSessionDataDeleg
|
|
|
117
110
|
("success", error == nil),
|
|
118
111
|
]))
|
|
119
112
|
}
|
|
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(.cancel)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
func urlSession(
|
|
156
|
-
_ session: URLSession,
|
|
157
|
-
dataTask: URLSessionDataTask,
|
|
158
|
-
didReceive data: Data
|
|
159
|
-
) {
|
|
160
|
-
forwardingDataDelegate?.urlSession?(
|
|
161
|
-
session,
|
|
162
|
-
dataTask: dataTask,
|
|
163
|
-
didReceive: data
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
func urlSession(
|
|
168
|
-
_ session: URLSession,
|
|
169
|
-
task: URLSessionTask,
|
|
170
|
-
didCompleteWithError error: Error?
|
|
171
|
-
) {
|
|
172
|
-
forwardingDataDelegate?.urlSession?(
|
|
173
|
-
session,
|
|
174
|
-
task: task,
|
|
175
|
-
didCompleteWithError: error
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
113
|
}
|
|
179
114
|
|
|
180
115
|
/// Core HTTPS client that enforces IP direct connection with SNI.
|
|
@@ -185,7 +120,7 @@ final class SniConnectClient {
|
|
|
185
120
|
private var activeTasksByToken: [UUID: Task<Response, Error>] = [:]
|
|
186
121
|
private var requestTokensById: [String: UUID] = [:]
|
|
187
122
|
private let tasksQueue = DispatchQueue(label: "com.onekey.sni.connect.tasks", attributes: .concurrent)
|
|
188
|
-
private let requestLimiter = SniConnectRequestLimiter
|
|
123
|
+
private let requestLimiter = SniConnectRequestLimiter.shared
|
|
189
124
|
|
|
190
125
|
private struct SessionKey: Hashable {
|
|
191
126
|
let hostname: String
|
|
@@ -399,10 +334,33 @@ final class SniConnectClient {
|
|
|
399
334
|
}
|
|
400
335
|
|
|
401
336
|
private static func makeURLSession(for key: SessionKey) throws -> ManagedSession {
|
|
402
|
-
let
|
|
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(
|
|
403
359
|
hostname: key.hostname,
|
|
404
360
|
ip: key.ip
|
|
405
361
|
)
|
|
362
|
+
|
|
363
|
+
EMASCurlProtocol.install(into: configuration, with: curlConfig)
|
|
406
364
|
SniConnectLog.info(SniConnectLog.event("sni_transport_config", [
|
|
407
365
|
("hostname", key.hostname),
|
|
408
366
|
("ipHash", SniConnectLog.shortHash(key.ip)),
|
|
@@ -413,13 +371,15 @@ final class SniConnectClient {
|
|
|
413
371
|
("followRedirects", false),
|
|
414
372
|
("cacheEnabled", false),
|
|
415
373
|
]))
|
|
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
|
+
)
|
|
416
380
|
return ManagedSession(
|
|
417
|
-
session: URLSession(
|
|
418
|
-
|
|
419
|
-
delegate: resources.delegate,
|
|
420
|
-
delegateQueue: nil
|
|
421
|
-
),
|
|
422
|
-
resolverLease: resources.resolverLease
|
|
381
|
+
session: URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil),
|
|
382
|
+
resolverLease: resolverLease
|
|
423
383
|
)
|
|
424
384
|
}
|
|
425
385
|
|
|
@@ -550,6 +510,10 @@ final class SniConnectClient {
|
|
|
550
510
|
]))
|
|
551
511
|
}
|
|
552
512
|
|
|
513
|
+
func debugSnapshot(hostname: String, ip: String) -> SniConnectRequestLimiter.Snapshot {
|
|
514
|
+
requestLimiter.snapshot(hostname: hostname, ip: ip)
|
|
515
|
+
}
|
|
516
|
+
|
|
553
517
|
/// Cancel a request by ID
|
|
554
518
|
func cancelRequest(requestId: String) -> Bool {
|
|
555
519
|
return tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
@@ -656,9 +620,24 @@ final class SniConnectClient {
|
|
|
656
620
|
|
|
657
621
|
let requestSlot: SniConnectRequestLimiter.Token
|
|
658
622
|
do {
|
|
659
|
-
|
|
623
|
+
// Admission wait and transport share one total wall-clock deadline.
|
|
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
|
+
}
|
|
660
639
|
} catch {
|
|
661
|
-
let sniError = SniConnectError.
|
|
640
|
+
let sniError = SniConnectError.from(error)
|
|
662
641
|
SniConnectLog.error(SniConnectLog.event("sni_request_result", [
|
|
663
642
|
("result", "error"),
|
|
664
643
|
("code", sniError.code),
|
|
@@ -676,6 +655,14 @@ final class SniConnectClient {
|
|
|
676
655
|
defer {
|
|
677
656
|
requestSlot.release()
|
|
678
657
|
}
|
|
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
|
+
}
|
|
679
666
|
|
|
680
667
|
let url = try Self.buildURL(hostname: config.hostname, normalizedPath: normalizedPath)
|
|
681
668
|
|
|
@@ -683,8 +670,9 @@ final class SniConnectClient {
|
|
|
683
670
|
mutableRequest.httpMethod = method
|
|
684
671
|
|
|
685
672
|
// Convert milliseconds to seconds for timeout values
|
|
686
|
-
let totalTimeoutSeconds =
|
|
687
|
-
let connectTimeoutSeconds =
|
|
673
|
+
let totalTimeoutSeconds = remainingTimeoutMilliseconds / 1000.0
|
|
674
|
+
let connectTimeoutSeconds =
|
|
675
|
+
min(config.effectiveConnectTimeout, remainingTimeoutMilliseconds) / 1000.0
|
|
688
676
|
|
|
689
677
|
// URLRequest.timeoutInterval is not a full request deadline. Keep it aligned
|
|
690
678
|
// with the caller timeout as a transport guard; the wall-clock deadline below
|
|
@@ -714,12 +702,15 @@ final class SniConnectClient {
|
|
|
714
702
|
("method", method),
|
|
715
703
|
("timeoutMs", Int(config.effectiveTotalTimeout)),
|
|
716
704
|
("connectTimeoutMs", Int(config.effectiveConnectTimeout)),
|
|
705
|
+
("queueWaitMs", queueWaitMilliseconds),
|
|
717
706
|
("headerCount", normalizedHeaders.count),
|
|
718
707
|
("bodyBytes", config.body?.data(using: .utf8)?.count ?? 0),
|
|
719
708
|
]))
|
|
720
709
|
|
|
721
710
|
do {
|
|
722
|
-
return try await SniConnectWallClockDeadline.run(
|
|
711
|
+
return try await SniConnectWallClockDeadline.run(
|
|
712
|
+
timeoutMilliseconds: remainingTimeoutMilliseconds
|
|
713
|
+
) {
|
|
723
714
|
let lease = try await self.sessionLease(for: config)
|
|
724
715
|
defer {
|
|
725
716
|
self.releaseSessionLease(lease)
|