@onekeyfe/react-native-sni-connect 3.0.85 → 3.0.87
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 +204 -60
- package/android/src/main/java/com/sniconnect/SniConnectRequestAdmission.kt +276 -0
- package/android/src/main/java/com/sniconnect/SniConnectValidation.kt +12 -78
- package/android/src/test/java/com/sniconnect/SniConnectRequestAdmissionTest.kt +309 -0
- package/android/src/test/java/com/sniconnect/SniConnectValidationTest.kt +23 -26
- package/ios/SniConnect.mm +27 -2
- package/ios/SniConnect.swift +34 -0
- package/ios/SniConnectClient.swift +102 -33
- package/ios/SniConnectCore.swift +49 -3
- package/ios/SniConnectValidation.swift +214 -41
- package/ios/Tests/SniConnectValidationTests/SniConnectValidationTests.swift +220 -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 +1 -1
- package/src/NativeSniConnect.ts +17 -0
- package/src/index.tsx +10 -0
|
@@ -32,6 +32,7 @@ enum SniConnectValidation {
|
|
|
32
32
|
static let maxTotalHeaderBytes = 32 * 1024
|
|
33
33
|
static let maxActiveRequests = 64
|
|
34
34
|
static let maxActiveRequestsPerPair = 16
|
|
35
|
+
static let maxPendingRequests = 256
|
|
35
36
|
|
|
36
37
|
/// HTTP methods the module is allowed to issue.
|
|
37
38
|
private static let allowedMethods: Set<String> = [
|
|
@@ -212,6 +213,16 @@ enum SniConnectValidation {
|
|
|
212
213
|
throw ValidationError.invalidIP(ip)
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
static func canonicalIPKey(_ ip: String) -> String {
|
|
217
|
+
if let v4 = parseIPv4(ip) {
|
|
218
|
+
return "4:" + v4.map { String($0) }.joined(separator: ".")
|
|
219
|
+
}
|
|
220
|
+
if let v6 = parseIPv6(ip) {
|
|
221
|
+
return "6:" + v6.map { String($0) }.joined(separator: ".")
|
|
222
|
+
}
|
|
223
|
+
return ip
|
|
224
|
+
}
|
|
225
|
+
|
|
215
226
|
private static func parseIPv4(_ ip: String) -> [UInt8]? {
|
|
216
227
|
var addr = in_addr()
|
|
217
228
|
guard ip.withCString({ inet_pton(AF_INET, $0, &addr) }) == 1 else { return nil }
|
|
@@ -301,16 +312,27 @@ enum SniConnectValidation {
|
|
|
301
312
|
}
|
|
302
313
|
}
|
|
303
314
|
|
|
304
|
-
final class SniConnectRequestLimiter {
|
|
305
|
-
|
|
315
|
+
final class SniConnectRequestLimiter: @unchecked Sendable {
|
|
316
|
+
static let shared = SniConnectRequestLimiter()
|
|
317
|
+
|
|
318
|
+
struct Snapshot {
|
|
319
|
+
let activeRequests: Int
|
|
320
|
+
let activeRequestsForPair: Int
|
|
321
|
+
let pendingRequests: Int
|
|
322
|
+
let pendingRequestsForPair: Int
|
|
323
|
+
let activeRequestIdsForPair: [String]
|
|
324
|
+
let pendingRequestIdsForPair: [String]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
final class Token: @unchecked Sendable {
|
|
306
328
|
private weak var limiter: SniConnectRequestLimiter?
|
|
307
|
-
private let
|
|
329
|
+
private let id: UUID
|
|
308
330
|
private let lock = NSLock()
|
|
309
331
|
private var released = false
|
|
310
332
|
|
|
311
|
-
fileprivate init(limiter: SniConnectRequestLimiter,
|
|
333
|
+
fileprivate init(limiter: SniConnectRequestLimiter, id: UUID) {
|
|
312
334
|
self.limiter = limiter
|
|
313
|
-
self.
|
|
335
|
+
self.id = id
|
|
314
336
|
}
|
|
315
337
|
|
|
316
338
|
func release() {
|
|
@@ -321,7 +343,7 @@ final class SniConnectRequestLimiter {
|
|
|
321
343
|
}
|
|
322
344
|
released = true
|
|
323
345
|
lock.unlock()
|
|
324
|
-
limiter?.release(
|
|
346
|
+
limiter?.release(id: id)
|
|
325
347
|
}
|
|
326
348
|
|
|
327
349
|
deinit {
|
|
@@ -329,65 +351,216 @@ final class SniConnectRequestLimiter {
|
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
|
|
354
|
+
private final class CancellationState: @unchecked Sendable {
|
|
355
|
+
private let lock = NSLock()
|
|
356
|
+
private var cancelled = false
|
|
357
|
+
|
|
358
|
+
var isCancelled: Bool {
|
|
359
|
+
lock.lock()
|
|
360
|
+
defer { lock.unlock() }
|
|
361
|
+
return cancelled
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func cancel() {
|
|
365
|
+
lock.lock()
|
|
366
|
+
cancelled = true
|
|
367
|
+
lock.unlock()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private struct PendingRequest {
|
|
372
|
+
let id: UUID
|
|
373
|
+
let key: String
|
|
374
|
+
let requestId: String?
|
|
375
|
+
let continuation: CheckedContinuation<Token, Error>
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private struct ActiveRequest {
|
|
379
|
+
let key: String
|
|
380
|
+
let requestId: String?
|
|
381
|
+
}
|
|
382
|
+
|
|
332
383
|
private let maxActiveRequests: Int
|
|
333
384
|
private let maxActiveRequestsPerPair: Int
|
|
385
|
+
private let maxPendingRequests: Int
|
|
334
386
|
private let queue = DispatchQueue(label: "com.onekey.sni.connect.request-limiter")
|
|
335
387
|
private var activeRequests = 0
|
|
336
388
|
private var activeRequestsByPair: [String: Int] = [:]
|
|
389
|
+
private var activeRequestsByID: [UUID: ActiveRequest] = [:]
|
|
390
|
+
private var pendingRequests: [PendingRequest] = []
|
|
337
391
|
|
|
338
392
|
init(
|
|
339
393
|
maxActiveRequests: Int = SniConnectValidation.maxActiveRequests,
|
|
340
|
-
maxActiveRequestsPerPair: Int = SniConnectValidation.maxActiveRequestsPerPair
|
|
394
|
+
maxActiveRequestsPerPair: Int = SniConnectValidation.maxActiveRequestsPerPair,
|
|
395
|
+
maxPendingRequests: Int = SniConnectValidation.maxPendingRequests
|
|
341
396
|
) {
|
|
342
397
|
self.maxActiveRequests = maxActiveRequests
|
|
343
398
|
self.maxActiveRequestsPerPair = maxActiveRequestsPerPair
|
|
399
|
+
self.maxPendingRequests = maxPendingRequests
|
|
344
400
|
}
|
|
345
401
|
|
|
346
|
-
|
|
402
|
+
var pendingRequestCount: Int {
|
|
403
|
+
queue.sync {
|
|
404
|
+
pendingRequests.count
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func snapshot(hostname: String, ip: String) -> Snapshot {
|
|
347
409
|
let key = pairKey(hostname: hostname, ip: ip)
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
410
|
+
return queue.sync {
|
|
411
|
+
let activeForPair = activeRequestsByID.values.filter { $0.key == key }
|
|
412
|
+
let pendingForPair = pendingRequests.filter { $0.key == key }
|
|
413
|
+
return Snapshot(
|
|
414
|
+
activeRequests: activeRequests,
|
|
415
|
+
activeRequestsForPair: activeForPair.count,
|
|
416
|
+
pendingRequests: pendingRequests.count,
|
|
417
|
+
pendingRequestsForPair: pendingForPair.count,
|
|
418
|
+
activeRequestIdsForPair: activeForPair.compactMap { $0.requestId }.sorted(),
|
|
419
|
+
pendingRequestIdsForPair: pendingForPair.compactMap { $0.requestId }.sorted()
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func acquire(hostname: String, ip: String, requestId: String? = nil) async throws -> Token {
|
|
425
|
+
try Task.checkCancellation()
|
|
426
|
+
|
|
427
|
+
let id = UUID()
|
|
428
|
+
let key = pairKey(hostname: hostname, ip: ip)
|
|
429
|
+
let trackedRequestId = requestId.flatMap { $0.isEmpty ? nil : $0 }
|
|
430
|
+
let cancellationState = CancellationState()
|
|
431
|
+
|
|
432
|
+
let token = try await withTaskCancellationHandler {
|
|
433
|
+
try Task.checkCancellation()
|
|
434
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
435
|
+
var immediateToken: Token?
|
|
436
|
+
var immediateError: Error?
|
|
437
|
+
|
|
438
|
+
queue.sync {
|
|
439
|
+
if cancellationState.isCancelled {
|
|
440
|
+
immediateError = CancellationError()
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if hasCapacity(for: key) {
|
|
445
|
+
retainSlot(id: id, key: key, requestId: trackedRequestId)
|
|
446
|
+
immediateToken = Token(limiter: self, id: id)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if pendingRequests.count >= maxPendingRequests {
|
|
451
|
+
SniConnectCoreDiagnostics.warn(SniConnectCoreDiagnostics.event("sni_resource_limit", [
|
|
452
|
+
("activeCount", activeRequests),
|
|
453
|
+
("pairCount", activeRequestsByPair[key] ?? 0),
|
|
454
|
+
("pendingCount", pendingRequests.count),
|
|
455
|
+
("limit", maxPendingRequests),
|
|
456
|
+
("reason", "max_pending_requests"),
|
|
457
|
+
("hostname", hostname.lowercased()),
|
|
458
|
+
("ipHash", SniConnectCoreDiagnostics.shortHash(ip)),
|
|
459
|
+
]))
|
|
460
|
+
immediateError = SniConnectValidation.ValidationError.resourceLimit(
|
|
461
|
+
"Too many pending SNI requests"
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
pendingRequests.append(PendingRequest(
|
|
467
|
+
id: id,
|
|
468
|
+
key: key,
|
|
469
|
+
requestId: trackedRequestId,
|
|
470
|
+
continuation: continuation
|
|
471
|
+
))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if let immediateToken {
|
|
475
|
+
continuation.resume(returning: immediateToken)
|
|
476
|
+
} else if let immediateError {
|
|
477
|
+
continuation.resume(throwing: immediateError)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} onCancel: {
|
|
481
|
+
cancellationState.cancel()
|
|
482
|
+
self.cancelPendingRequest(id: id)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Cancellation can race with a pending-to-active handoff. Never return a
|
|
486
|
+
// token to an already-cancelled caller without releasing its retained slot.
|
|
487
|
+
do {
|
|
488
|
+
try Task.checkCancellation()
|
|
489
|
+
return token
|
|
490
|
+
} catch {
|
|
491
|
+
token.release()
|
|
492
|
+
throw error
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private func release(id: UUID) {
|
|
497
|
+
var nextRequest: PendingRequest?
|
|
498
|
+
|
|
499
|
+
queue.sync {
|
|
500
|
+
guard releaseSlot(id: id) else { return }
|
|
501
|
+
|
|
502
|
+
guard activeRequests < maxActiveRequests,
|
|
503
|
+
let index = pendingRequests.firstIndex(where: { hasCapacity(for: $0.key) }) else {
|
|
504
|
+
return
|
|
359
505
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
("ipHash", SniConnectCoreDiagnostics.shortHash(ip)),
|
|
369
|
-
]))
|
|
370
|
-
throw SniConnectValidation.ValidationError.resourceLimit("Too many active SNI requests for destination")
|
|
506
|
+
|
|
507
|
+
nextRequest = pendingRequests.remove(at: index)
|
|
508
|
+
if let nextRequest {
|
|
509
|
+
retainSlot(
|
|
510
|
+
id: nextRequest.id,
|
|
511
|
+
key: nextRequest.key,
|
|
512
|
+
requestId: nextRequest.requestId
|
|
513
|
+
)
|
|
371
514
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if let nextRequest {
|
|
518
|
+
nextRequest.continuation.resume(returning: Token(limiter: self, id: nextRequest.id))
|
|
375
519
|
}
|
|
376
520
|
}
|
|
377
521
|
|
|
378
|
-
private func
|
|
522
|
+
private func cancelPendingRequest(id: UUID) {
|
|
523
|
+
var cancelledRequest: PendingRequest?
|
|
524
|
+
|
|
379
525
|
queue.sync {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if pairCount <= 1 {
|
|
383
|
-
activeRequestsByPair.removeValue(forKey: key)
|
|
384
|
-
} else {
|
|
385
|
-
activeRequestsByPair[key] = pairCount - 1
|
|
526
|
+
guard let index = pendingRequests.firstIndex(where: { $0.id == id }) else {
|
|
527
|
+
return
|
|
386
528
|
}
|
|
529
|
+
cancelledRequest = pendingRequests.remove(at: index)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
cancelledRequest?.continuation.resume(throwing: CancellationError())
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private func hasCapacity(for key: String) -> Bool {
|
|
536
|
+
activeRequests < maxActiveRequests &&
|
|
537
|
+
(activeRequestsByPair[key] ?? 0) < maxActiveRequestsPerPair
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private func retainSlot(id: UUID, key: String, requestId: String?) {
|
|
541
|
+
activeRequestsByID[id] = ActiveRequest(key: key, requestId: requestId)
|
|
542
|
+
activeRequests += 1
|
|
543
|
+
activeRequestsByPair[key, default: 0] += 1
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private func releaseSlot(id: UUID) -> Bool {
|
|
547
|
+
guard let activeRequest = activeRequestsByID.removeValue(forKey: id) else {
|
|
548
|
+
return false
|
|
549
|
+
}
|
|
550
|
+
let key = activeRequest.key
|
|
551
|
+
activeRequests = max(0, activeRequests - 1)
|
|
552
|
+
guard let pairCount = activeRequestsByPair[key] else {
|
|
553
|
+
return true
|
|
554
|
+
}
|
|
555
|
+
if pairCount <= 1 {
|
|
556
|
+
activeRequestsByPair.removeValue(forKey: key)
|
|
557
|
+
} else {
|
|
558
|
+
activeRequestsByPair[key] = pairCount - 1
|
|
387
559
|
}
|
|
560
|
+
return true
|
|
388
561
|
}
|
|
389
562
|
|
|
390
563
|
private func pairKey(hostname: String, ip: String) -> String {
|
|
391
|
-
return "\(hostname.lowercased())|\(ip)"
|
|
564
|
+
return "\(hostname.lowercased())|\(SniConnectValidation.canonicalIPKey(ip))"
|
|
392
565
|
}
|
|
393
566
|
}
|
|
@@ -122,12 +122,18 @@ final class SniConnectValidationTests: XCTestCase {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
func testEnforcesRequestIdTimeoutAndBodyLimits() {
|
|
125
|
+
XCTAssertNoThrow(
|
|
126
|
+
try SniConnectValidation.validateRequestId(String(repeating: "界", count: 42))
|
|
127
|
+
)
|
|
125
128
|
assertValidationFails {
|
|
126
129
|
try SniConnectValidation.validateRequestId("")
|
|
127
130
|
}
|
|
128
131
|
assertValidationFails {
|
|
129
132
|
try SniConnectValidation.validateRequestId(String(repeating: "x", count: 129))
|
|
130
133
|
}
|
|
134
|
+
assertValidationFails {
|
|
135
|
+
try SniConnectValidation.validateRequestId(String(repeating: "界", count: 43))
|
|
136
|
+
}
|
|
131
137
|
assertValidationFails {
|
|
132
138
|
try SniConnectValidation.validateRequestId("req\n1")
|
|
133
139
|
}
|
|
@@ -204,24 +210,196 @@ final class SniConnectValidationTests: XCTestCase {
|
|
|
204
210
|
XCTAssertNoThrow(try SniConnectValidation.validateMethodBody(method: "OPTIONS", body: nil))
|
|
205
211
|
}
|
|
206
212
|
|
|
207
|
-
func
|
|
213
|
+
func testTwentySamePairRequestsProduceSixteenActiveAndFourPending() async throws {
|
|
214
|
+
let limiter = SniConnectRequestLimiter()
|
|
215
|
+
var tasks: [String: Task<SniConnectRequestLimiter.Token, Error>] = [:]
|
|
216
|
+
|
|
217
|
+
for index in 0..<20 {
|
|
218
|
+
let requestId = String(format: "req-%02d", index)
|
|
219
|
+
tasks[requestId] = Task {
|
|
220
|
+
try await limiter.acquire(
|
|
221
|
+
hostname: "Example.com",
|
|
222
|
+
ip: index.isMultiple(of: 2)
|
|
223
|
+
? "2001:4860:4860::8888"
|
|
224
|
+
: "2001:4860:4860:0:0:0:0:8888",
|
|
225
|
+
requestId: requestId
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await waitForPendingRequests(4, in: limiter)
|
|
231
|
+
let saturated = limiter.snapshot(
|
|
232
|
+
hostname: "EXAMPLE.COM",
|
|
233
|
+
ip: "2001:4860:4860::8888"
|
|
234
|
+
)
|
|
235
|
+
XCTAssertEqual(saturated.activeRequests, 16)
|
|
236
|
+
XCTAssertEqual(saturated.activeRequestsForPair, 16)
|
|
237
|
+
XCTAssertEqual(saturated.pendingRequests, 4)
|
|
238
|
+
XCTAssertEqual(saturated.pendingRequestsForPair, 4)
|
|
239
|
+
XCTAssertEqual(saturated.activeRequestIdsForPair.count, 16)
|
|
240
|
+
XCTAssertEqual(saturated.pendingRequestIdsForPair.count, 4)
|
|
241
|
+
|
|
242
|
+
for requestId in saturated.pendingRequestIdsForPair {
|
|
243
|
+
tasks[requestId]?.cancel()
|
|
244
|
+
}
|
|
245
|
+
for requestId in saturated.pendingRequestIdsForPair {
|
|
246
|
+
do {
|
|
247
|
+
_ = try await tasks[requestId]!.value
|
|
248
|
+
XCTFail("Expected pending request \(requestId) to be cancelled")
|
|
249
|
+
} catch is CancellationError {
|
|
250
|
+
// Expected.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
XCTAssertEqual(limiter.pendingRequestCount, 0)
|
|
255
|
+
for requestId in saturated.activeRequestIdsForPair {
|
|
256
|
+
let token = try await tasks[requestId]!.value
|
|
257
|
+
token.release()
|
|
258
|
+
}
|
|
259
|
+
let drained = limiter.snapshot(
|
|
260
|
+
hostname: "example.com",
|
|
261
|
+
ip: "2001:4860:4860:0:0:0:0:8888"
|
|
262
|
+
)
|
|
263
|
+
XCTAssertEqual(drained.activeRequests, 0)
|
|
264
|
+
XCTAssertEqual(drained.pendingRequests, 0)
|
|
265
|
+
|
|
266
|
+
let recovery = try await limiter.acquire(
|
|
267
|
+
hostname: "example.com",
|
|
268
|
+
ip: "2001:4860:4860::8888",
|
|
269
|
+
requestId: "recovery"
|
|
270
|
+
)
|
|
271
|
+
recovery.release()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func testRequestLimiterQueuesGlobalAndPerDestinationLimits() async throws {
|
|
208
275
|
let limiter = SniConnectRequestLimiter(maxActiveRequests: 2, maxActiveRequestsPerPair: 1)
|
|
209
|
-
let firstToken = try limiter.acquire(hostname: "Example.com", ip: "93.184.216.34")
|
|
276
|
+
let firstToken = try await limiter.acquire(hostname: "Example.com", ip: "93.184.216.34")
|
|
210
277
|
|
|
211
|
-
|
|
212
|
-
|
|
278
|
+
let sameDestinationTask = Task {
|
|
279
|
+
try await limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
|
|
213
280
|
}
|
|
281
|
+
await waitForPendingRequests(1, in: limiter)
|
|
214
282
|
|
|
215
|
-
let secondToken = try limiter.acquire(hostname: "example.com", ip: "93.184.216.35")
|
|
216
|
-
|
|
217
|
-
|
|
283
|
+
let secondToken = try await limiter.acquire(hostname: "example.com", ip: "93.184.216.35")
|
|
284
|
+
let globallyQueuedTask = Task {
|
|
285
|
+
try await limiter.acquire(hostname: "example.net", ip: "93.184.216.36")
|
|
218
286
|
}
|
|
287
|
+
await waitForPendingRequests(2, in: limiter)
|
|
219
288
|
|
|
220
289
|
firstToken.release()
|
|
221
|
-
let replacementToken = try
|
|
290
|
+
let replacementToken = try await sameDestinationTask.value
|
|
222
291
|
firstToken.release()
|
|
223
292
|
secondToken.release()
|
|
293
|
+
let globallyQueuedToken = try await globallyQueuedTask.value
|
|
224
294
|
replacementToken.release()
|
|
295
|
+
globallyQueuedToken.release()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func testRequestLimiterSnapshotCanonicalizesTargetAndReturnsRequestIDs() async throws {
|
|
299
|
+
let limiter = SniConnectRequestLimiter(maxActiveRequests: 2, maxActiveRequestsPerPair: 1)
|
|
300
|
+
let activeTarget = try await limiter.acquire(
|
|
301
|
+
hostname: "Example.com",
|
|
302
|
+
ip: "2001:4860:4860::8888",
|
|
303
|
+
requestId: "active-target"
|
|
304
|
+
)
|
|
305
|
+
let activeOther = try await limiter.acquire(
|
|
306
|
+
hostname: "example.com",
|
|
307
|
+
ip: "93.184.216.34",
|
|
308
|
+
requestId: "active-other"
|
|
309
|
+
)
|
|
310
|
+
let pendingTargetTask = Task {
|
|
311
|
+
try await limiter.acquire(
|
|
312
|
+
hostname: "example.com",
|
|
313
|
+
ip: "2001:4860:4860:0:0:0:0:8888",
|
|
314
|
+
requestId: "pending-target"
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
let pendingWithoutIDTask = Task {
|
|
318
|
+
try await limiter.acquire(
|
|
319
|
+
hostname: "example.com",
|
|
320
|
+
ip: "2001:4860:4860::8888",
|
|
321
|
+
requestId: ""
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
await waitForPendingRequests(2, in: limiter)
|
|
325
|
+
|
|
326
|
+
let snapshot = limiter.snapshot(
|
|
327
|
+
hostname: "EXAMPLE.COM",
|
|
328
|
+
ip: "2001:4860:4860:0:0:0:0:8888"
|
|
329
|
+
)
|
|
330
|
+
XCTAssertEqual(snapshot.activeRequests, 2)
|
|
331
|
+
XCTAssertEqual(snapshot.activeRequestsForPair, 1)
|
|
332
|
+
XCTAssertEqual(snapshot.pendingRequests, 2)
|
|
333
|
+
XCTAssertEqual(snapshot.pendingRequestsForPair, 2)
|
|
334
|
+
XCTAssertEqual(snapshot.activeRequestIdsForPair, ["active-target"])
|
|
335
|
+
XCTAssertEqual(snapshot.pendingRequestIdsForPair, ["pending-target"])
|
|
336
|
+
|
|
337
|
+
pendingTargetTask.cancel()
|
|
338
|
+
pendingWithoutIDTask.cancel()
|
|
339
|
+
_ = try? await pendingTargetTask.value
|
|
340
|
+
_ = try? await pendingWithoutIDTask.value
|
|
341
|
+
activeTarget.release()
|
|
342
|
+
activeOther.release()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func testRequestLimiterRejectsTheTwoHundredFiftySeventhPendingRequest() async throws {
|
|
346
|
+
let limiter = SniConnectRequestLimiter(
|
|
347
|
+
maxActiveRequests: 1,
|
|
348
|
+
maxActiveRequestsPerPair: 1,
|
|
349
|
+
maxPendingRequests: 256
|
|
350
|
+
)
|
|
351
|
+
let activeToken = try await limiter.acquire(
|
|
352
|
+
hostname: "example.com",
|
|
353
|
+
ip: "93.184.216.34"
|
|
354
|
+
)
|
|
355
|
+
let queuedTasks = (0..<256).map { index in
|
|
356
|
+
Task {
|
|
357
|
+
try await limiter.acquire(
|
|
358
|
+
hostname: "example.com",
|
|
359
|
+
ip: "93.184.216.34",
|
|
360
|
+
requestId: "pending-\(index)"
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
await waitForPendingRequests(256, in: limiter)
|
|
365
|
+
|
|
366
|
+
do {
|
|
367
|
+
_ = try await limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
|
|
368
|
+
XCTFail("Expected the bounded pending queue to reject overflow")
|
|
369
|
+
} catch SniConnectValidation.ValidationError.resourceLimit(_) {
|
|
370
|
+
// Expected.
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
queuedTasks.forEach { $0.cancel() }
|
|
374
|
+
for task in queuedTasks {
|
|
375
|
+
_ = try? await task.value
|
|
376
|
+
}
|
|
377
|
+
XCTAssertEqual(limiter.pendingRequestCount, 0)
|
|
378
|
+
activeToken.release()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
func testWallClockDeadlineCancelsPendingLimiterWait() async throws {
|
|
382
|
+
let limiter = SniConnectRequestLimiter(maxActiveRequests: 1, maxActiveRequestsPerPair: 1)
|
|
383
|
+
let activeToken = try await limiter.acquire(
|
|
384
|
+
hostname: "example.com",
|
|
385
|
+
ip: "93.184.216.34"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
do {
|
|
389
|
+
_ = try await SniConnectWallClockDeadline.run(timeoutMilliseconds: 20) {
|
|
390
|
+
try await limiter.acquire(
|
|
391
|
+
hostname: "example.com",
|
|
392
|
+
ip: "93.184.216.34",
|
|
393
|
+
requestId: "deadline-pending"
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
XCTFail("Expected queue wait to consume the wall-clock deadline")
|
|
397
|
+
} catch let error as SniConnectTimeout {
|
|
398
|
+
XCTAssertEqual(error, .deadlineExceeded)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
XCTAssertEqual(limiter.pendingRequestCount, 0)
|
|
402
|
+
activeToken.release()
|
|
225
403
|
}
|
|
226
404
|
|
|
227
405
|
func testResolverRegistryKeepsResolverClassUntilAllSessionsReleaseIt() throws {
|
|
@@ -298,6 +476,20 @@ final class SniConnectValidationTests: XCTestCase {
|
|
|
298
476
|
}
|
|
299
477
|
}
|
|
300
478
|
|
|
479
|
+
func testWallClockDeadlineRejectsResultAfterAbsoluteDeadline() async throws {
|
|
480
|
+
let deadline = SniConnectWallClockDeadline.makeDeadline(timeoutMilliseconds: 5)
|
|
481
|
+
try await Task.sleep(nanoseconds: 20_000_000)
|
|
482
|
+
|
|
483
|
+
do {
|
|
484
|
+
_ = try await SniConnectWallClockDeadline.run(until: deadline) {
|
|
485
|
+
return "late"
|
|
486
|
+
}
|
|
487
|
+
XCTFail("Expected an operation result produced after the deadline to be rejected")
|
|
488
|
+
} catch let error as SniConnectTimeout {
|
|
489
|
+
XCTAssertEqual(error, .deadlineExceeded)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
301
493
|
func testResponseHeaderMapsPreserveRawRepeatedSetCookieHeaders() throws {
|
|
302
494
|
let headerMaps = SniConnectResponseHeaders.make(rawHeaderFields: [
|
|
303
495
|
(name: "Content-Type", value: "application/json"),
|
|
@@ -331,4 +523,24 @@ final class SniConnectValidationTests: XCTestCase {
|
|
|
331
523
|
private func assertValidationFails(_ block: () throws -> Void, file: StaticString = #filePath, line: UInt = #line) {
|
|
332
524
|
XCTAssertThrowsError(try block(), file: file, line: line)
|
|
333
525
|
}
|
|
526
|
+
|
|
527
|
+
private func waitForPendingRequests(
|
|
528
|
+
_ expectedCount: Int,
|
|
529
|
+
in limiter: SniConnectRequestLimiter,
|
|
530
|
+
file: StaticString = #filePath,
|
|
531
|
+
line: UInt = #line
|
|
532
|
+
) async {
|
|
533
|
+
let deadline = Date().addingTimeInterval(5)
|
|
534
|
+
while Date() < deadline {
|
|
535
|
+
if limiter.pendingRequestCount == expectedCount {
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
try? await Task.sleep(nanoseconds: 1_000_000)
|
|
539
|
+
}
|
|
540
|
+
XCTFail(
|
|
541
|
+
"Expected \(expectedCount) pending requests, got \(limiter.pendingRequestCount)",
|
|
542
|
+
file: file,
|
|
543
|
+
line: line
|
|
544
|
+
)
|
|
545
|
+
}
|
|
334
546
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -13,6 +13,9 @@ export function cancelAllRequests() {
|
|
|
13
13
|
export function clearDNSCache() {
|
|
14
14
|
return NativeSniConnect.clearDNSCache();
|
|
15
15
|
}
|
|
16
|
+
export function getDebugSnapshot(target) {
|
|
17
|
+
return NativeSniConnect.getDebugSnapshot(target);
|
|
18
|
+
}
|
|
16
19
|
export function isProxyActiveForUrl(url) {
|
|
17
20
|
return NativeSniConnect.isProxyActiveForUrl(url);
|
|
18
21
|
}
|
|
@@ -39,6 +39,18 @@ export type SniConnectResponse = {
|
|
|
39
39
|
headers: HeaderMap;
|
|
40
40
|
multiValueHeaders?: MultiValueHeaderMap;
|
|
41
41
|
};
|
|
42
|
+
export type SniConnectDebugTarget = {
|
|
43
|
+
ip: string;
|
|
44
|
+
hostname: string;
|
|
45
|
+
};
|
|
46
|
+
export type SniConnectDebugSnapshot = {
|
|
47
|
+
activeRequests: Int32;
|
|
48
|
+
activeRequestsForPair: Int32;
|
|
49
|
+
pendingRequests: Int32;
|
|
50
|
+
pendingRequestsForPair: Int32;
|
|
51
|
+
activeRequestIdsForPair: string[];
|
|
52
|
+
pendingRequestIdsForPair: string[];
|
|
53
|
+
};
|
|
42
54
|
export interface Spec extends TurboModule {
|
|
43
55
|
request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
|
|
44
56
|
cancelRequest(requestId: string): Promise<{
|
|
@@ -50,6 +62,7 @@ export interface Spec extends TurboModule {
|
|
|
50
62
|
clearDNSCache(): Promise<{
|
|
51
63
|
success: boolean;
|
|
52
64
|
}>;
|
|
65
|
+
getDebugSnapshot(target: SniConnectDebugTarget): Promise<SniConnectDebugSnapshot>;
|
|
53
66
|
isProxyActiveForUrl(url: string): Promise<boolean>;
|
|
54
67
|
}
|
|
55
68
|
export type SniConnectModule = Spec;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SniConnectRequest, type SniConnectResponse } from './NativeSniConnect';
|
|
1
|
+
import { type SniConnectDebugSnapshot, type SniConnectDebugTarget, type SniConnectRequest, type SniConnectResponse } from './NativeSniConnect';
|
|
2
2
|
export declare function request(config: SniConnectRequest): Promise<SniConnectResponse>;
|
|
3
3
|
export declare function cancelRequest(requestId: string): Promise<{
|
|
4
4
|
success: boolean;
|
|
@@ -9,6 +9,7 @@ export declare function cancelAllRequests(): Promise<{
|
|
|
9
9
|
export declare function clearDNSCache(): Promise<{
|
|
10
10
|
success: boolean;
|
|
11
11
|
}>;
|
|
12
|
+
export declare function getDebugSnapshot(target: SniConnectDebugTarget): Promise<SniConnectDebugSnapshot>;
|
|
12
13
|
export declare function isProxyActiveForUrl(url: string): Promise<boolean>;
|
|
13
|
-
export type { SniConnectBodylessMethod, SniConnectMethod, SniConnectOptionalBodyMethod, SniConnectRequest, SniConnectRequiredBodyMethod, SniConnectResponse, } from './NativeSniConnect';
|
|
14
|
+
export type { SniConnectBodylessMethod, SniConnectDebugSnapshot, SniConnectDebugTarget, SniConnectMethod, SniConnectOptionalBodyMethod, SniConnectRequest, SniConnectRequiredBodyMethod, SniConnectResponse, } from './NativeSniConnect';
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
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.87",
|
|
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",
|
package/src/NativeSniConnect.ts
CHANGED
|
@@ -58,11 +58,28 @@ export type SniConnectResponse = {
|
|
|
58
58
|
multiValueHeaders?: MultiValueHeaderMap;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
export type SniConnectDebugTarget = {
|
|
62
|
+
ip: string;
|
|
63
|
+
hostname: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type SniConnectDebugSnapshot = {
|
|
67
|
+
activeRequests: Int32;
|
|
68
|
+
activeRequestsForPair: Int32;
|
|
69
|
+
pendingRequests: Int32;
|
|
70
|
+
pendingRequestsForPair: Int32;
|
|
71
|
+
activeRequestIdsForPair: string[];
|
|
72
|
+
pendingRequestIdsForPair: string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
61
75
|
export interface Spec extends TurboModule {
|
|
62
76
|
request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
|
|
63
77
|
cancelRequest(requestId: string): Promise<{ success: boolean }>;
|
|
64
78
|
cancelAllRequests(): Promise<{ success: boolean }>;
|
|
65
79
|
clearDNSCache(): Promise<{ success: boolean }>;
|
|
80
|
+
getDebugSnapshot(
|
|
81
|
+
target: SniConnectDebugTarget
|
|
82
|
+
): Promise<SniConnectDebugSnapshot>;
|
|
66
83
|
isProxyActiveForUrl(url: string): Promise<boolean>;
|
|
67
84
|
}
|
|
68
85
|
|