@onekeyfe/react-native-sni-connect 3.0.81 → 3.0.85

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/src/index.tsx CHANGED
@@ -1,6 +1,4 @@
1
1
  import NativeSniConnect, {
2
- type SniConnectDebugSnapshot,
3
- type SniConnectDebugTarget,
4
2
  type SniConnectRequest,
5
3
  type SniConnectResponse,
6
4
  } from './NativeSniConnect';
@@ -25,20 +23,12 @@ export function clearDNSCache(): Promise<{ success: boolean }> {
25
23
  return NativeSniConnect.clearDNSCache();
26
24
  }
27
25
 
28
- export function getDebugSnapshot(
29
- target: SniConnectDebugTarget
30
- ): Promise<SniConnectDebugSnapshot> {
31
- return NativeSniConnect.getDebugSnapshot(target);
32
- }
33
-
34
26
  export function isProxyActiveForUrl(url: string): Promise<boolean> {
35
27
  return NativeSniConnect.isProxyActiveForUrl(url);
36
28
  }
37
29
 
38
30
  export type {
39
31
  SniConnectBodylessMethod,
40
- SniConnectDebugSnapshot,
41
- SniConnectDebugTarget,
42
32
  SniConnectMethod,
43
33
  SniConnectOptionalBodyMethod,
44
34
  SniConnectRequest,
@@ -1,276 +0,0 @@
1
- package com.sniconnect
2
-
3
- import java.util.ArrayDeque
4
- import java.util.Locale
5
- import java.util.concurrent.ScheduledExecutorService
6
- import java.util.concurrent.ScheduledFuture
7
- import java.util.concurrent.ScheduledThreadPoolExecutor
8
- import java.util.concurrent.TimeUnit
9
-
10
- internal data class SniConnectAdmissionSnapshot(
11
- val activeRequests: Int,
12
- val activeRequestsForPair: Int,
13
- val pendingRequests: Int,
14
- val pendingRequestsForPair: Int,
15
- val activeRequestIdsForPair: List<String> = emptyList(),
16
- val pendingRequestIdsForPair: List<String> = emptyList(),
17
- )
18
-
19
- internal class SniConnectRequestAdmission(
20
- private val maxActiveRequests: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS,
21
- private val maxActiveRequestsPerPair: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS_PER_PAIR,
22
- private val maxPendingRequests: Int = SniConnectValidation.MAX_PENDING_REQUESTS,
23
- private val scheduler: ScheduledExecutorService = createAdmissionScheduler(),
24
- private val nanoTime: () -> Long = System::nanoTime,
25
- ) {
26
- internal data class PairKey(
27
- val hostname: String,
28
- val ip: String,
29
- )
30
-
31
- internal enum class State {
32
- CREATED,
33
- PENDING,
34
- ACTIVE,
35
- TERMINAL,
36
- }
37
-
38
- private data class Dispatch(
39
- val ticket: Ticket,
40
- val remainingTimeoutMillis: Long?,
41
- )
42
-
43
- inner class Ticket internal constructor(
44
- internal val pair: PairKey,
45
- internal val requestId: String?,
46
- internal val deadlineNanos: Long,
47
- internal val onAdmitted: (remainingTimeoutMillis: Long) -> Unit,
48
- internal val onPendingFailure: (code: String, message: String) -> Unit,
49
- ) {
50
- internal var state = State.CREATED
51
- internal var timeoutFuture: ScheduledFuture<*>? = null
52
-
53
- fun submit() {
54
- this@SniConnectRequestAdmission.submit(this)
55
- }
56
-
57
- fun cancelPending(): Boolean =
58
- this@SniConnectRequestAdmission.cancelPending(this)
59
-
60
- fun release() {
61
- this@SniConnectRequestAdmission.release(this)
62
- }
63
- }
64
-
65
- private val lock = Any()
66
- private var activeRequests = 0
67
- private val activeRequestsByPair = mutableMapOf<PairKey, Int>()
68
- private val activeTickets = mutableSetOf<Ticket>()
69
- private val pendingRequests = ArrayDeque<Ticket>()
70
-
71
- fun createTicket(
72
- hostname: String,
73
- ip: String,
74
- requestId: String? = null,
75
- timeoutMillis: Long,
76
- onAdmitted: (remainingTimeoutMillis: Long) -> Unit,
77
- onPendingFailure: (code: String, message: String) -> Unit,
78
- ): Ticket = Ticket(
79
- pair = pairKey(hostname, ip),
80
- requestId = requestId?.takeIf { it.isNotEmpty() },
81
- deadlineNanos = nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis),
82
- onAdmitted = onAdmitted,
83
- onPendingFailure = onPendingFailure,
84
- )
85
-
86
- fun snapshot(hostname: String, ip: String): SniConnectAdmissionSnapshot {
87
- val pair = pairKey(hostname, ip)
88
- return synchronized(lock) {
89
- val activeForPair = activeTickets.filter { ticket -> ticket.pair == pair }
90
- val pendingForPair = pendingRequests.filter { ticket -> ticket.pair == pair }
91
- SniConnectAdmissionSnapshot(
92
- activeRequests = activeRequests,
93
- activeRequestsForPair = activeForPair.size,
94
- pendingRequests = pendingRequests.size,
95
- pendingRequestsForPair = pendingForPair.size,
96
- activeRequestIdsForPair = activeForPair.mapNotNull { ticket -> ticket.requestId }.sorted(),
97
- pendingRequestIdsForPair = pendingForPair.mapNotNull { ticket -> ticket.requestId }.sorted(),
98
- )
99
- }
100
- }
101
-
102
- internal fun shutdownForTests() {
103
- scheduler.shutdownNow()
104
- }
105
-
106
- private fun submit(ticket: Ticket) {
107
- var remainingTimeoutMillis: Long? = null
108
- var timedOut = false
109
- synchronized(lock) {
110
- // A runtime can cancel immediately after registering the handle but
111
- // before submit() reaches this lock. Cancellation already settled the
112
- // ticket, so submission becomes an idempotent no-op.
113
- if (ticket.state == State.TERMINAL) return
114
- check(ticket.state == State.CREATED) { "Admission ticket already submitted" }
115
- if (ticket.deadlineNanos <= nanoTime()) {
116
- ticket.state = State.TERMINAL
117
- timedOut = true
118
- } else if (canActivateLocked(ticket.pair)) {
119
- activateLocked(ticket)
120
- remainingTimeoutMillis = remainingMillis(ticket.deadlineNanos)
121
- } else {
122
- if (pendingRequests.size >= maxPendingRequests) {
123
- ticket.state = State.TERMINAL
124
- throw SniConnectValidation.ValidationException("Too many pending SNI requests")
125
- }
126
- ticket.state = State.PENDING
127
- pendingRequests.addLast(ticket)
128
- val delayNanos = (ticket.deadlineNanos - nanoTime()).coerceAtLeast(0L)
129
- ticket.timeoutFuture = scheduler.schedule(
130
- { timeoutPending(ticket) },
131
- delayNanos,
132
- TimeUnit.NANOSECONDS,
133
- )
134
- }
135
- }
136
-
137
- if (timedOut) {
138
- ticket.onPendingFailure(
139
- "SNI_REQUEST_TIMEOUT",
140
- "Request timed out while waiting for admission",
141
- )
142
- } else {
143
- remainingTimeoutMillis?.let(ticket.onAdmitted)
144
- }
145
- }
146
-
147
- private fun cancelPending(ticket: Ticket): Boolean {
148
- val cancelled = synchronized(lock) {
149
- when (ticket.state) {
150
- State.CREATED -> {
151
- ticket.state = State.TERMINAL
152
- true
153
- }
154
- State.PENDING -> {
155
- pendingRequests.remove(ticket)
156
- ticket.timeoutFuture?.cancel(false)
157
- ticket.timeoutFuture = null
158
- ticket.state = State.TERMINAL
159
- true
160
- }
161
- State.ACTIVE, State.TERMINAL -> false
162
- }
163
- }
164
- if (cancelled) {
165
- ticket.onPendingFailure("SNI_CANCELLED", "Request cancelled")
166
- }
167
- return cancelled
168
- }
169
-
170
- private fun timeoutPending(ticket: Ticket) {
171
- val timedOut = synchronized(lock) {
172
- if (ticket.state != State.PENDING) {
173
- false
174
- } else {
175
- pendingRequests.remove(ticket)
176
- ticket.timeoutFuture = null
177
- ticket.state = State.TERMINAL
178
- true
179
- }
180
- }
181
- if (timedOut) {
182
- ticket.onPendingFailure(
183
- "SNI_REQUEST_TIMEOUT",
184
- "Request timed out while waiting for admission",
185
- )
186
- }
187
- }
188
-
189
- private fun release(ticket: Ticket) {
190
- val admissions = synchronized(lock) {
191
- if (ticket.state != State.ACTIVE) return
192
- ticket.state = State.TERMINAL
193
- activeTickets.remove(ticket)
194
- activeRequests -= 1
195
- decrementPairLocked(ticket.pair)
196
- collectAdmissionsLocked()
197
- }
198
- admissions.forEach { dispatch ->
199
- val timeoutMillis = dispatch.remainingTimeoutMillis
200
- if (timeoutMillis == null) {
201
- dispatch.ticket.onPendingFailure(
202
- "SNI_REQUEST_TIMEOUT",
203
- "Request timed out while waiting for admission",
204
- )
205
- } else {
206
- dispatch.ticket.onAdmitted(timeoutMillis)
207
- }
208
- }
209
- }
210
-
211
- private fun collectAdmissionsLocked(): List<Dispatch> {
212
- val admissions = mutableListOf<Dispatch>()
213
- while (activeRequests < maxActiveRequests) {
214
- val iterator = pendingRequests.iterator()
215
- var next: Ticket? = null
216
- while (iterator.hasNext()) {
217
- val candidate = iterator.next()
218
- if (canActivateLocked(candidate.pair)) {
219
- iterator.remove()
220
- next = candidate
221
- break
222
- }
223
- }
224
- val ticket = next ?: break
225
- ticket.timeoutFuture?.cancel(false)
226
- ticket.timeoutFuture = null
227
- if (ticket.deadlineNanos <= nanoTime()) {
228
- ticket.state = State.TERMINAL
229
- admissions += Dispatch(ticket, null)
230
- } else {
231
- activateLocked(ticket)
232
- admissions += Dispatch(ticket, remainingMillis(ticket.deadlineNanos))
233
- }
234
- }
235
- return admissions
236
- }
237
-
238
- private fun canActivateLocked(pair: PairKey): Boolean =
239
- activeRequests < maxActiveRequests &&
240
- (activeRequestsByPair[pair] ?: 0) < maxActiveRequestsPerPair
241
-
242
- private fun pairKey(hostname: String, ip: String): PairKey = PairKey(
243
- hostname = hostname.lowercase(Locale.US),
244
- ip = SniConnectValidation.canonicalizePublicIp(ip),
245
- )
246
-
247
- private fun activateLocked(ticket: Ticket) {
248
- ticket.state = State.ACTIVE
249
- activeTickets.add(ticket)
250
- activeRequests += 1
251
- activeRequestsByPair[ticket.pair] = (activeRequestsByPair[ticket.pair] ?: 0) + 1
252
- }
253
-
254
- private fun decrementPairLocked(pair: PairKey) {
255
- val pairCount = activeRequestsByPair[pair] ?: return
256
- if (pairCount == 1) {
257
- activeRequestsByPair.remove(pair)
258
- } else {
259
- activeRequestsByPair[pair] = pairCount - 1
260
- }
261
- }
262
-
263
- private fun remainingMillis(deadlineNanos: Long): Long {
264
- val remainingNanos = (deadlineNanos - nanoTime()).coerceAtLeast(1L)
265
- return ((remainingNanos + 999_999L) / 1_000_000L).coerceAtLeast(1L)
266
- }
267
-
268
- private companion object {
269
- fun createAdmissionScheduler(): ScheduledExecutorService =
270
- ScheduledThreadPoolExecutor(1) { runnable ->
271
- Thread(runnable, "SniConnectAdmission").apply { isDaemon = true }
272
- }.apply {
273
- removeOnCancelPolicy = true
274
- }
275
- }
276
- }
@@ -1,299 +0,0 @@
1
- package com.sniconnect
2
-
3
- import java.util.Collections
4
- import java.util.concurrent.CountDownLatch
5
- import java.util.concurrent.TimeUnit
6
- import org.junit.Assert.assertEquals
7
- import org.junit.Assert.assertFalse
8
- import org.junit.Assert.assertTrue
9
- import org.junit.Test
10
-
11
- class SniConnectRequestAdmissionTest {
12
- @Test
13
- fun twentySamePairRequestsProduceSixteenActiveAndFourPending() {
14
- val admission = SniConnectRequestAdmission()
15
- val admitted = mutableListOf<String>()
16
- val failures = mutableListOf<Pair<String, String>>()
17
- try {
18
- val tickets = (0 until 20).map { index ->
19
- val requestId = "req-${index.toString().padStart(2, '0')}"
20
- admission.createTicket(
21
- hostname = "Example.com",
22
- ip = if (index % 2 == 0) "093.184.216.034" else "93.184.216.34",
23
- requestId = requestId,
24
- timeoutMillis = 10_000,
25
- onAdmitted = { admitted += requestId },
26
- onPendingFailure = { code, message -> failures += code to message },
27
- ).also { it.submit() }
28
- }
29
-
30
- assertEquals((0 until 16).map { "req-${it.toString().padStart(2, '0')}" }, admitted)
31
- assertEquals(
32
- SniConnectAdmissionSnapshot(
33
- activeRequests = 16,
34
- activeRequestsForPair = 16,
35
- pendingRequests = 4,
36
- pendingRequestsForPair = 4,
37
- activeRequestIdsForPair = (0 until 16).map { "req-${it.toString().padStart(2, '0')}" },
38
- pendingRequestIdsForPair = (16 until 20).map { "req-${it.toString().padStart(2, '0')}" },
39
- ),
40
- admission.snapshot("EXAMPLE.COM", "93.184.216.34"),
41
- )
42
-
43
- tickets.drop(16).forEach { pending -> assertTrue(pending.cancelPending()) }
44
- assertEquals(4, failures.size)
45
- assertTrue(failures.all { it.first == "SNI_CANCELLED" })
46
- assertEquals(0, admission.snapshot("example.com", "093.184.216.034").pendingRequests)
47
-
48
- tickets.take(16).forEach { active -> active.release() }
49
- assertEquals(
50
- SniConnectAdmissionSnapshot(0, 0, 0, 0),
51
- admission.snapshot("example.com", "93.184.216.34"),
52
- )
53
-
54
- val recovery = ticket(admission, "example.com", "93.184.216.34")
55
- recovery.submit()
56
- assertEquals(1, admission.snapshot("example.com", "93.184.216.34").activeRequests)
57
- recovery.release()
58
- } finally {
59
- admission.shutdownForTests()
60
- }
61
- }
62
-
63
- @Test
64
- fun queuesAtGlobalLimitAndDispatchesAfterRelease() {
65
- val admission = admission(maxActive = 2, maxPerPair = 2)
66
- val admitted = mutableListOf<String>()
67
- try {
68
- val first = submit(admission, "first", "one.example", "93.184.216.34", admitted)
69
- val second = submit(admission, "second", "two.example", "93.184.216.35", admitted)
70
- val pending = submit(admission, "pending", "three.example", "93.184.216.36", admitted)
71
-
72
- assertEquals(listOf("first", "second"), admitted)
73
- assertEquals(2, admission.snapshot("three.example", "93.184.216.36").activeRequests)
74
- assertEquals(1, admission.snapshot("three.example", "93.184.216.36").pendingRequests)
75
-
76
- first.release()
77
- assertEquals(listOf("first", "second", "pending"), admitted)
78
- first.release()
79
- second.release()
80
- pending.release()
81
- assertEquals(0, admission.snapshot("three.example", "93.184.216.36").activeRequests)
82
- } finally {
83
- admission.shutdownForTests()
84
- }
85
- }
86
-
87
- @Test
88
- fun sameHostnameDifferentIpsHaveIndependentPairLimits() {
89
- val admission = admission(maxActive = 4, maxPerPair = 2)
90
- val admitted = mutableListOf<String>()
91
- try {
92
- val firstA = submit(admission, "a1", "Example.com", "93.184.216.34", admitted)
93
- val secondA = submit(admission, "a2", "example.com", "93.184.216.34", admitted)
94
- val pendingA = submit(admission, "a3", "example.com", "93.184.216.34", admitted)
95
- val firstB = submit(admission, "b1", "example.com", "93.184.216.35", admitted)
96
- val secondB = submit(admission, "b2", "example.com", "93.184.216.35", admitted)
97
-
98
- assertEquals(listOf("a1", "a2", "b1", "b2"), admitted)
99
- assertEquals(
100
- SniConnectAdmissionSnapshot(
101
- activeRequests = 4,
102
- activeRequestsForPair = 2,
103
- pendingRequests = 1,
104
- pendingRequestsForPair = 1,
105
- activeRequestIdsForPair = listOf("a1", "a2"),
106
- pendingRequestIdsForPair = listOf("a3"),
107
- ),
108
- admission.snapshot("EXAMPLE.COM", "93.184.216.34"),
109
- )
110
-
111
- firstA.release()
112
- assertEquals(listOf("a1", "a2", "b1", "b2", "a3"), admitted)
113
- secondA.release()
114
- pendingA.release()
115
- firstB.release()
116
- secondB.release()
117
- assertEquals(
118
- SniConnectAdmissionSnapshot(0, 0, 0, 0),
119
- admission.snapshot("example.com", "93.184.216.34"),
120
- )
121
- } finally {
122
- admission.shutdownForTests()
123
- }
124
- }
125
-
126
- @Test
127
- fun rejectsTheTwoHundredFiftySeventhPendingRequest() {
128
- val admission = admission(maxActive = 1, maxPerPair = 1, maxPending = 256)
129
- try {
130
- val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
131
- val pending = (0 until 256).map {
132
- ticket(admission, "example.com", "93.184.216.34").also { request -> request.submit() }
133
- }
134
- val overflow = ticket(admission, "example.com", "93.184.216.34")
135
-
136
- assertValidationFails { overflow.submit() }
137
- assertEquals(256, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
138
-
139
- pending.forEach { request -> assertTrue(request.cancelPending()) }
140
- active.release()
141
- assertEquals(
142
- SniConnectAdmissionSnapshot(0, 0, 0, 0),
143
- admission.snapshot("example.com", "93.184.216.34"),
144
- )
145
- } finally {
146
- admission.shutdownForTests()
147
- }
148
- }
149
-
150
- @Test
151
- fun cancellingPendingRequestRemovesItAndSettlesImmediately() {
152
- val admission = admission(maxActive = 1, maxPerPair = 1)
153
- val failures = mutableListOf<Pair<String, String>>()
154
- try {
155
- val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
156
- val pending = admission.createTicket(
157
- hostname = "example.com",
158
- ip = "93.184.216.34",
159
- timeoutMillis = 10_000,
160
- onAdmitted = { throw AssertionError("Cancelled request must not be admitted") },
161
- onPendingFailure = { code, message -> failures += code to message },
162
- ).also { it.submit() }
163
-
164
- assertTrue(pending.cancelPending())
165
- assertEquals(listOf("SNI_CANCELLED" to "Request cancelled"), failures)
166
- assertEquals(0, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
167
- assertFalse(pending.cancelPending())
168
- active.release()
169
- } finally {
170
- admission.shutdownForTests()
171
- }
172
- }
173
-
174
- @Test
175
- fun cancellationBeforeSubmitSettlesOnceAndSubmitBecomesNoOp() {
176
- val admission = admission(maxActive = 1, maxPerPair = 1)
177
- val failures = mutableListOf<String>()
178
- try {
179
- val request = admission.createTicket(
180
- hostname = "example.com",
181
- ip = "93.184.216.34",
182
- timeoutMillis = 10_000,
183
- onAdmitted = { throw AssertionError("Cancelled request must not be admitted") },
184
- onPendingFailure = { code, _ -> failures += code },
185
- )
186
-
187
- assertTrue(request.cancelPending())
188
- request.submit()
189
- assertEquals(listOf("SNI_CANCELLED"), failures)
190
- assertEquals(
191
- SniConnectAdmissionSnapshot(0, 0, 0, 0),
192
- admission.snapshot("example.com", "93.184.216.34"),
193
- )
194
- } finally {
195
- admission.shutdownForTests()
196
- }
197
- }
198
-
199
- @Test
200
- fun timeoutIncludesTimeSpentWaitingForAdmission() {
201
- val admission = admission(maxActive = 1, maxPerPair = 1)
202
- val timedOut = CountDownLatch(1)
203
- val failureCodes = Collections.synchronizedList(mutableListOf<String>())
204
- try {
205
- val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
206
- admission.createTicket(
207
- hostname = "example.com",
208
- ip = "93.184.216.34",
209
- timeoutMillis = 40,
210
- onAdmitted = { throw AssertionError("Timed-out request must not be admitted") },
211
- onPendingFailure = { code, _ ->
212
- failureCodes += code
213
- timedOut.countDown()
214
- },
215
- ).submit()
216
-
217
- assertTrue(timedOut.await(2, TimeUnit.SECONDS))
218
- assertEquals(listOf("SNI_REQUEST_TIMEOUT"), failureCodes.toList())
219
- assertEquals(0, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
220
- active.release()
221
- } finally {
222
- admission.shutdownForTests()
223
- }
224
- }
225
-
226
- @Test
227
- fun admittedRequestReceivesOnlyTheRemainingTimeout() {
228
- var nowNanos = 0L
229
- val admission = admission(
230
- maxActive = 1,
231
- maxPerPair = 1,
232
- nanoTime = { nowNanos },
233
- )
234
- val remainingTimeouts = mutableListOf<Long>()
235
- try {
236
- val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
237
- val pending = admission.createTicket(
238
- hostname = "example.com",
239
- ip = "93.184.216.34",
240
- timeoutMillis = 1_000,
241
- onAdmitted = { remainingTimeout -> remainingTimeouts += remainingTimeout },
242
- onPendingFailure = { code, _ -> throw AssertionError("Unexpected failure: $code") },
243
- ).also { it.submit() }
244
-
245
- nowNanos = TimeUnit.MILLISECONDS.toNanos(250)
246
- active.release()
247
-
248
- assertEquals(listOf(750L), remainingTimeouts)
249
- pending.release()
250
- } finally {
251
- admission.shutdownForTests()
252
- }
253
- }
254
-
255
- private fun admission(
256
- maxActive: Int,
257
- maxPerPair: Int,
258
- maxPending: Int = 10,
259
- nanoTime: () -> Long = System::nanoTime,
260
- ) = SniConnectRequestAdmission(
261
- maxActiveRequests = maxActive,
262
- maxActiveRequestsPerPair = maxPerPair,
263
- maxPendingRequests = maxPending,
264
- nanoTime = nanoTime,
265
- )
266
-
267
- private fun submit(
268
- admission: SniConnectRequestAdmission,
269
- id: String,
270
- hostname: String,
271
- ip: String,
272
- admitted: MutableList<String>,
273
- ): SniConnectRequestAdmission.Ticket =
274
- ticket(admission, hostname, ip, requestId = id, onAdmitted = { admitted += id }).also { it.submit() }
275
-
276
- private fun ticket(
277
- admission: SniConnectRequestAdmission,
278
- hostname: String,
279
- ip: String,
280
- requestId: String? = null,
281
- onAdmitted: (Long) -> Unit = {},
282
- ): SniConnectRequestAdmission.Ticket = admission.createTicket(
283
- hostname = hostname,
284
- ip = ip,
285
- requestId = requestId,
286
- timeoutMillis = 60_000,
287
- onAdmitted = onAdmitted,
288
- onPendingFailure = { _, _ -> },
289
- )
290
-
291
- private fun assertValidationFails(block: () -> Unit) {
292
- try {
293
- block()
294
- } catch (_: SniConnectValidation.ValidationException) {
295
- return
296
- }
297
- throw AssertionError("Expected SNI validation failure")
298
- }
299
- }