@onekeyfe/react-native-sni-connect 3.0.70 → 3.0.72

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.
@@ -0,0 +1,325 @@
1
+ import XCTest
2
+ @testable import SniConnectValidationCore
3
+
4
+ final class SniConnectValidationTests: XCTestCase {
5
+
6
+ func testAcceptsValidRequestBoundaryValues() throws {
7
+ XCTAssertNoThrow(try SniConnectValidation.validateRequestId("req-1"))
8
+ XCTAssertNoThrow(try SniConnectValidation.validateTimeout(120_000))
9
+ XCTAssertNoThrow(try SniConnectValidation.validateBody(String(repeating: "a", count: 1024 * 1024)))
10
+ XCTAssertNoThrow(try SniConnectValidation.validatePublicIP("93.184.216.34"))
11
+ XCTAssertNoThrow(try SniConnectValidation.validatePublicIP("2001:4860:4860::8888"))
12
+ XCTAssertNoThrow(try SniConnectValidation.validateHostname("api.example.com"))
13
+
14
+ XCTAssertEqual(try SniConnectValidation.normalizeMethod(" get "), "GET")
15
+ XCTAssertEqual(try SniConnectValidation.normalizePath(""), "/")
16
+ XCTAssertEqual(try SniConnectValidation.normalizePath("v1?q=1"), "/v1?q=1")
17
+ }
18
+
19
+ func testRejectsIpLiteralHostnames() {
20
+ assertValidationFails {
21
+ try SniConnectValidation.validateHostname("93.184.216.34")
22
+ }
23
+ assertValidationFails {
24
+ try SniConnectValidation.validateHostname("2001:4860:4860::8888")
25
+ }
26
+ }
27
+
28
+ func testRejectsMalformedHostnames() {
29
+ [
30
+ "",
31
+ "-example.com",
32
+ "example-.com",
33
+ "example..com",
34
+ "bad_host.example",
35
+ "https://example.com",
36
+ "example.com:443",
37
+ String(repeating: "a", count: 64) + ".example.com",
38
+ String(repeating: "a", count: 250) + ".com",
39
+ ].forEach { hostname in
40
+ assertValidationFails {
41
+ try SniConnectValidation.validateHostname(hostname)
42
+ }
43
+ }
44
+ }
45
+
46
+ func testRejectsUnsafeIpv4Destinations() {
47
+ [
48
+ "example.com",
49
+ "93.184.216.34:443",
50
+ " 93.184.216.34",
51
+ "10.0.0.1",
52
+ "127.0.0.1",
53
+ "100.64.0.1",
54
+ "169.254.169.254",
55
+ "172.16.0.1",
56
+ "192.168.1.1",
57
+ "192.0.2.1",
58
+ "198.18.0.1",
59
+ "198.51.100.1",
60
+ "203.0.113.1",
61
+ "224.0.0.1",
62
+ "255.255.255.255",
63
+ ].forEach { ip in
64
+ assertValidationFails {
65
+ try SniConnectValidation.validatePublicIP(ip)
66
+ }
67
+ }
68
+ }
69
+
70
+ func testRejectsUnsafeIpv6DestinationsAndTransitionForms() {
71
+ [
72
+ "::",
73
+ "::1",
74
+ "fe80::1",
75
+ "fc00::1",
76
+ "ff00::1",
77
+ "100::1",
78
+ "2001::1",
79
+ "2001:2::1",
80
+ "2001:db8::1",
81
+ "2002:0a00:0001::1",
82
+ "::ffff:10.0.0.1",
83
+ "64:ff9b::10.0.0.1",
84
+ "64:ff9b:1::1",
85
+ "2001:4860:4860::8888%en0",
86
+ "[2001:4860:4860::8888]",
87
+ ].forEach { ip in
88
+ assertValidationFails {
89
+ try SniConnectValidation.validatePublicIP(ip)
90
+ }
91
+ }
92
+ }
93
+
94
+ func testRejectsUnsupportedMethodsAndUnsafePaths() {
95
+ ["TRACE", "CONNECT", "", "GET\n"].forEach { method in
96
+ assertValidationFails {
97
+ _ = try SniConnectValidation.normalizeMethod(method)
98
+ }
99
+ }
100
+
101
+ [
102
+ "https://example.com",
103
+ "http://example.com",
104
+ "//example.com/path",
105
+ "javascript:alert(1)",
106
+ "/path\nInjected: yes",
107
+ "/" + String(repeating: "a", count: 8192),
108
+ ].forEach { path in
109
+ assertValidationFails {
110
+ _ = try SniConnectValidation.normalizePath(path)
111
+ }
112
+ }
113
+ }
114
+
115
+ func testEnforcesRequestIdTimeoutAndBodyLimits() {
116
+ assertValidationFails {
117
+ try SniConnectValidation.validateRequestId("")
118
+ }
119
+ assertValidationFails {
120
+ try SniConnectValidation.validateRequestId(String(repeating: "x", count: 129))
121
+ }
122
+ assertValidationFails {
123
+ try SniConnectValidation.validateRequestId("req\n1")
124
+ }
125
+ assertValidationFails {
126
+ try SniConnectValidation.validateTimeout(0)
127
+ }
128
+ assertValidationFails {
129
+ try SniConnectValidation.validateTimeout(120_001)
130
+ }
131
+ assertValidationFails {
132
+ try SniConnectValidation.validateTimeout(.nan)
133
+ }
134
+ assertValidationFails {
135
+ try SniConnectValidation.validateBody(String(repeating: "a", count: 1024 * 1024 + 1))
136
+ }
137
+ }
138
+
139
+ func testFiltersModuleOwnedHeadersAndRejectsUnsafeHeaders() throws {
140
+ let normalized = try SniConnectValidation.normalizeHeaders([
141
+ "Host": "evil.example",
142
+ "Content-Length": "9999",
143
+ "Accept-Encoding": "gzip",
144
+ "x-emascurl-config-id": "evil",
145
+ "X-Test": "ok",
146
+ ])
147
+
148
+ XCTAssertFalse(normalized.keys.contains { $0.caseInsensitiveCompare("host") == .orderedSame })
149
+ XCTAssertFalse(normalized.keys.contains { $0.caseInsensitiveCompare("content-length") == .orderedSame })
150
+ XCTAssertFalse(normalized.keys.contains { $0.caseInsensitiveCompare("accept-encoding") == .orderedSame })
151
+ XCTAssertEqual(normalized["X-Test"], "ok")
152
+
153
+ [
154
+ ["Connection": "close"],
155
+ ["Proxy-Authorization": "secret"],
156
+ ["Transfer-Encoding": "chunked"],
157
+ ["Expect": "100-continue"],
158
+ [":authority": "evil.example"],
159
+ ["Bad Header": "x"],
160
+ ["X-Test": "line\nbreak"],
161
+ ["X-Test": String(repeating: "x", count: 8 * 1024 + 1)],
162
+ ].forEach { headers in
163
+ assertValidationFails {
164
+ _ = try SniConnectValidation.normalizeHeaders(headers)
165
+ }
166
+ }
167
+
168
+ assertValidationFails {
169
+ _ = try SniConnectValidation.normalizeHeaders(Dictionary(uniqueKeysWithValues: (0...64).map { ("X-\($0)", "v") }))
170
+ }
171
+ assertValidationFails {
172
+ _ = try SniConnectValidation.normalizeHeaders(Dictionary(uniqueKeysWithValues: (0...4).map { ("X-\($0)", String(repeating: "x", count: 7 * 1024)) }))
173
+ }
174
+ }
175
+
176
+ func testRejectsAmbiguousMethodBodyCombinations() throws {
177
+ assertValidationFails {
178
+ try SniConnectValidation.validateMethodBody(method: "GET", body: "")
179
+ }
180
+ assertValidationFails {
181
+ try SniConnectValidation.validateMethodBody(method: "HEAD", body: "payload")
182
+ }
183
+ assertValidationFails {
184
+ try SniConnectValidation.validateMethodBody(method: "POST", body: nil)
185
+ }
186
+ assertValidationFails {
187
+ try SniConnectValidation.validateMethodBody(method: "PUT", body: nil)
188
+ }
189
+ assertValidationFails {
190
+ try SniConnectValidation.validateMethodBody(method: "PATCH", body: nil)
191
+ }
192
+
193
+ XCTAssertNoThrow(try SniConnectValidation.validateMethodBody(method: "POST", body: ""))
194
+ XCTAssertNoThrow(try SniConnectValidation.validateMethodBody(method: "DELETE", body: nil))
195
+ XCTAssertNoThrow(try SniConnectValidation.validateMethodBody(method: "OPTIONS", body: nil))
196
+ }
197
+
198
+ func testRequestLimiterEnforcesGlobalAndPerDestinationLimits() throws {
199
+ let limiter = SniConnectRequestLimiter(maxActiveRequests: 2, maxActiveRequestsPerPair: 1)
200
+ let firstToken = try limiter.acquire(hostname: "Example.com", ip: "93.184.216.34")
201
+
202
+ assertValidationFails {
203
+ _ = try limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
204
+ }
205
+
206
+ let secondToken = try limiter.acquire(hostname: "example.com", ip: "93.184.216.35")
207
+ assertValidationFails {
208
+ _ = try limiter.acquire(hostname: "example.net", ip: "93.184.216.36")
209
+ }
210
+
211
+ firstToken.release()
212
+ let replacementToken = try limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
213
+ firstToken.release()
214
+ secondToken.release()
215
+ replacementToken.release()
216
+ }
217
+
218
+ func testResolverRegistryKeepsResolverClassUntilAllSessionsReleaseIt() throws {
219
+ final class ResolverA: NSObject {}
220
+ final class ResolverB: NSObject {}
221
+ var classes: [AnyClass] = [ResolverA.self, ResolverB.self]
222
+ let registry = SniConnectPinnedResolverRegistry(maxEntries: 2)
223
+
224
+ let first: AnyClass = try registry.resolverClass(hostname: "Example.com", ip: "93.184.216.34") {
225
+ classes.removeFirst()
226
+ }
227
+ let same: AnyClass = try registry.resolverClass(hostname: "example.com", ip: "93.184.216.34") {
228
+ XCTFail("Expected resolver class reuse for identical hostname/ip")
229
+ return ResolverB.self
230
+ }
231
+ XCTAssertTrue(first === same)
232
+ XCTAssertEqual(registry.resolve(domain: "example.com", resolverClass: first), "93.184.216.34")
233
+ XCTAssertEqual(registry.entryCount, 1)
234
+ XCTAssertEqual(registry.allocatedClassCount, 1)
235
+
236
+ _ = try registry.resolverClass(hostname: "example.com", ip: "93.184.216.35") {
237
+ classes.removeFirst()
238
+ }
239
+ XCTAssertEqual(registry.entryCount, 2)
240
+ XCTAssertEqual(registry.allocatedClassCount, 2)
241
+ assertValidationFails {
242
+ _ = try registry.resolverClass(hostname: "example.net", ip: "93.184.216.36") {
243
+ XCTFail("Bounded registry must not allocate past capacity")
244
+ return ResolverB.self
245
+ }
246
+ }
247
+
248
+ registry.release(hostname: "example.com", ip: "93.184.216.34")
249
+ XCTAssertEqual(registry.entryCount, 2)
250
+ XCTAssertEqual(registry.resolve(domain: "example.com", resolverClass: first), "93.184.216.34")
251
+
252
+ registry.release(hostname: "example.com", ip: "93.184.216.34")
253
+ XCTAssertEqual(registry.entryCount, 1)
254
+
255
+ registry.release(hostname: "example.com", ip: "93.184.216.35")
256
+ XCTAssertEqual(registry.entryCount, 0)
257
+ let reused: AnyClass = try registry.resolverClass(hostname: "example.net", ip: "93.184.216.36") {
258
+ XCTFail("Expected cleared resolver class to be reused")
259
+ return ResolverB.self
260
+ }
261
+ XCTAssertTrue(reused === ResolverA.self || reused === ResolverB.self)
262
+ XCTAssertEqual(registry.allocatedClassCount, 2)
263
+ XCTAssertEqual(registry.resolve(domain: "example.net", resolverClass: reused), "93.184.216.36")
264
+ }
265
+
266
+ func testResponseTextDecodePreservesOriginalJsonText() throws {
267
+ let json = "{ \"b\": 1, \"a\": [true, null] }\n"
268
+ XCTAssertEqual(SniConnectResponseText.decode(Data(json.utf8)), json)
269
+ XCTAssertEqual(SniConnectResponseText.decode(Data()), "")
270
+ }
271
+
272
+ func testWallClockDeadlineReturnsFastOperation() async throws {
273
+ let value = try await SniConnectWallClockDeadline.run(timeoutMilliseconds: 1_000) {
274
+ return "ok"
275
+ }
276
+
277
+ XCTAssertEqual(value, "ok")
278
+ }
279
+
280
+ func testWallClockDeadlineTimesOutSlowOperation() async throws {
281
+ do {
282
+ _ = try await SniConnectWallClockDeadline.run(timeoutMilliseconds: 10) {
283
+ try await Task.sleep(nanoseconds: 100_000_000)
284
+ return "late"
285
+ }
286
+ XCTFail("Expected wall-clock deadline to throw")
287
+ } catch let error as SniConnectTimeout {
288
+ XCTAssertEqual(error, .deadlineExceeded)
289
+ }
290
+ }
291
+
292
+ func testResponseHeaderMapsPreserveRawRepeatedSetCookieHeaders() throws {
293
+ let headerMaps = SniConnectResponseHeaders.make(rawHeaderFields: [
294
+ (name: "Content-Type", value: "application/json"),
295
+ (name: "Set-Cookie", value: "session=one; Path=/; HttpOnly"),
296
+ (name: "set-cookie", value: "theme=dark; Path=/; Secure"),
297
+ ])
298
+
299
+ XCTAssertEqual(headerMaps.singleValueHeaders["content-type"], "application/json")
300
+ XCTAssertEqual(headerMaps.singleValueHeaders["set-cookie"], "theme=dark; Path=/; Secure")
301
+ XCTAssertEqual(headerMaps.multiValueHeaders["set-cookie"], [
302
+ "session=one; Path=/; HttpOnly",
303
+ "theme=dark; Path=/; Secure",
304
+ ])
305
+ }
306
+
307
+ func testResponseHeaderMapsPreserveArrayBackedHeaderFields() throws {
308
+ let headerMaps = SniConnectResponseHeaders.make(from: [
309
+ "Set-Cookie": [
310
+ "session=one; Path=/; HttpOnly",
311
+ "theme=dark; Path=/; Secure",
312
+ ],
313
+ ])
314
+
315
+ XCTAssertEqual(headerMaps.singleValueHeaders["set-cookie"], "theme=dark; Path=/; Secure")
316
+ XCTAssertEqual(headerMaps.multiValueHeaders["set-cookie"], [
317
+ "session=one; Path=/; HttpOnly",
318
+ "theme=dark; Path=/; Secure",
319
+ ])
320
+ }
321
+
322
+ private func assertValidationFails(_ block: () throws -> Void, file: StaticString = #filePath, line: UInt = #line) {
323
+ XCTAssertThrowsError(try block(), file: file, line: line)
324
+ }
325
+ }
@@ -13,4 +13,7 @@ export function cancelAllRequests() {
13
13
  export function clearDNSCache() {
14
14
  return NativeSniConnect.clearDNSCache();
15
15
  }
16
+ export function isProxyActiveForUrl(url) {
17
+ return NativeSniConnect.isProxyActiveForUrl(url);
18
+ }
16
19
  //# sourceMappingURL=index.js.map
@@ -3,24 +3,44 @@ import type { Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
3
3
  type HeaderMap = {
4
4
  [key: string]: string;
5
5
  };
6
- export type SniConnectRequest = {
6
+ type MultiValueHeaderMap = {
7
+ [key: string]: string[];
8
+ };
9
+ type SniConnectRequestBase = {
7
10
  requestId?: string;
8
11
  ip: string;
9
12
  hostname: string;
10
- method: string;
11
13
  path: string;
12
14
  headers: HeaderMap;
13
- body?: string | null;
14
15
  timeout: Double;
15
16
  };
17
+ type NativeSniConnectRequest = SniConnectRequestBase & {
18
+ method: string;
19
+ body?: string | null;
20
+ };
21
+ export type SniConnectBodylessMethod = 'GET' | 'HEAD';
22
+ export type SniConnectRequiredBodyMethod = 'POST' | 'PUT' | 'PATCH';
23
+ export type SniConnectOptionalBodyMethod = 'DELETE' | 'OPTIONS';
24
+ export type SniConnectMethod = SniConnectBodylessMethod | SniConnectRequiredBodyMethod | SniConnectOptionalBodyMethod;
25
+ export type SniConnectRequest = (SniConnectRequestBase & {
26
+ method: SniConnectBodylessMethod;
27
+ body?: never;
28
+ }) | (SniConnectRequestBase & {
29
+ method: SniConnectRequiredBodyMethod;
30
+ body: string;
31
+ }) | (SniConnectRequestBase & {
32
+ method: SniConnectOptionalBodyMethod;
33
+ body?: string | null;
34
+ });
16
35
  export type SniConnectResponse = {
17
36
  data: string;
18
37
  status: Int32;
19
38
  statusText: string;
20
39
  headers: HeaderMap;
40
+ multiValueHeaders?: MultiValueHeaderMap;
21
41
  };
22
42
  export interface Spec extends TurboModule {
23
- request(config: SniConnectRequest): Promise<SniConnectResponse>;
43
+ request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
24
44
  cancelRequest(requestId: string): Promise<{
25
45
  success: boolean;
26
46
  }>;
@@ -30,6 +50,7 @@ export interface Spec extends TurboModule {
30
50
  clearDNSCache(): Promise<{
31
51
  success: boolean;
32
52
  }>;
53
+ isProxyActiveForUrl(url: string): Promise<boolean>;
33
54
  }
34
55
  export type SniConnectModule = Spec;
35
56
  declare const NativeSniConnect: SniConnectModule;
@@ -9,5 +9,6 @@ export declare function cancelAllRequests(): Promise<{
9
9
  export declare function clearDNSCache(): Promise<{
10
10
  success: boolean;
11
11
  }>;
12
- export type { SniConnectRequest, SniConnectResponse } from './NativeSniConnect';
12
+ export declare function isProxyActiveForUrl(url: string): Promise<boolean>;
13
+ export type { SniConnectBodylessMethod, SniConnectMethod, SniConnectOptionalBodyMethod, SniConnectRequest, SniConnectRequiredBodyMethod, SniConnectResponse, } from './NativeSniConnect';
13
14
  //# 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.70",
3
+ "version": "3.0.72",
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",
@@ -36,6 +36,9 @@
36
36
  "typecheck": "tsc",
37
37
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
38
38
  "test": "jest",
39
+ "test:android": "cd ../../example/react-native/android && ./gradlew :onekeyfe_react-native-sni-connect:testDebugUnitTest --no-daemon",
40
+ "test:ios": "swift test",
41
+ "test:native": "yarn test:ios && yarn test:android",
39
42
  "release": "yarn prepare && npm whoami && npm publish --access public"
40
43
  },
41
44
  "keywords": [
@@ -109,6 +112,11 @@
109
112
  "jsSrcsDir": "src",
110
113
  "android": {
111
114
  "javaPackageName": "com.sniconnect"
115
+ },
116
+ "ios": {
117
+ "modulesProvider": {
118
+ "SniConnect": "SniConnect"
119
+ }
112
120
  }
113
121
  },
114
122
  "prettier": {
@@ -10,29 +10,60 @@ type HeaderMap = {
10
10
  [key: string]: string;
11
11
  };
12
12
 
13
- export type SniConnectRequest = {
13
+ type MultiValueHeaderMap = {
14
+ [key: string]: string[];
15
+ };
16
+
17
+ type SniConnectRequestBase = {
14
18
  requestId?: string;
15
19
  ip: string;
16
20
  hostname: string;
17
- method: string;
18
21
  path: string;
19
22
  headers: HeaderMap;
20
- body?: string | null;
21
23
  timeout: Double;
22
24
  };
23
25
 
26
+ type NativeSniConnectRequest = SniConnectRequestBase & {
27
+ method: string;
28
+ body?: string | null;
29
+ };
30
+
31
+ export type SniConnectBodylessMethod = 'GET' | 'HEAD';
32
+ export type SniConnectRequiredBodyMethod = 'POST' | 'PUT' | 'PATCH';
33
+ export type SniConnectOptionalBodyMethod = 'DELETE' | 'OPTIONS';
34
+ export type SniConnectMethod =
35
+ | SniConnectBodylessMethod
36
+ | SniConnectRequiredBodyMethod
37
+ | SniConnectOptionalBodyMethod;
38
+
39
+ export type SniConnectRequest =
40
+ | (SniConnectRequestBase & {
41
+ method: SniConnectBodylessMethod;
42
+ body?: never;
43
+ })
44
+ | (SniConnectRequestBase & {
45
+ method: SniConnectRequiredBodyMethod;
46
+ body: string;
47
+ })
48
+ | (SniConnectRequestBase & {
49
+ method: SniConnectOptionalBodyMethod;
50
+ body?: string | null;
51
+ });
52
+
24
53
  export type SniConnectResponse = {
25
54
  data: string;
26
55
  status: Int32;
27
56
  statusText: string;
28
57
  headers: HeaderMap;
58
+ multiValueHeaders?: MultiValueHeaderMap;
29
59
  };
30
60
 
31
61
  export interface Spec extends TurboModule {
32
- request(config: SniConnectRequest): Promise<SniConnectResponse>;
62
+ request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
33
63
  cancelRequest(requestId: string): Promise<{ success: boolean }>;
34
64
  cancelAllRequests(): Promise<{ success: boolean }>;
35
65
  clearDNSCache(): Promise<{ success: boolean }>;
66
+ isProxyActiveForUrl(url: string): Promise<boolean>;
36
67
  }
37
68
 
38
69
  const LINKING_ERROR =
package/src/index.tsx CHANGED
@@ -23,4 +23,15 @@ export function clearDNSCache(): Promise<{ success: boolean }> {
23
23
  return NativeSniConnect.clearDNSCache();
24
24
  }
25
25
 
26
- export type { SniConnectRequest, SniConnectResponse } from './NativeSniConnect';
26
+ export function isProxyActiveForUrl(url: string): Promise<boolean> {
27
+ return NativeSniConnect.isProxyActiveForUrl(url);
28
+ }
29
+
30
+ export type {
31
+ SniConnectBodylessMethod,
32
+ SniConnectMethod,
33
+ SniConnectOptionalBodyMethod,
34
+ SniConnectRequest,
35
+ SniConnectRequiredBodyMethod,
36
+ SniConnectResponse,
37
+ } from './NativeSniConnect';