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

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.
@@ -1,30 +1,34 @@
1
1
  import Foundation
2
+ import UIKit
2
3
  import EMASCurl
3
4
 
4
5
  /// Core HTTPS client that enforces IP direct connection with SNI.
5
6
  final class SniConnectClient {
6
7
 
7
- // Logging callback
8
- var onLog: ((String, String) -> Void)?
9
-
10
8
  // Active requests tracking for cancellation support
11
9
  private var activeTasks: [String: Task<Response, Error>] = [:]
12
10
  private let tasksQueue = DispatchQueue(label: "com.onekey.sni.connect.tasks", attributes: .concurrent)
13
11
 
12
+ // Token for the memory-warning observer (block-based observers are not removed
13
+ // by `removeObserver(self)`, so the token must be retained and removed explicitly).
14
+ private var memoryWarningObserver: NSObjectProtocol?
15
+
14
16
  init() {
15
17
  // Register for memory warnings to clean cache
16
- NotificationCenter.default.addObserver(
18
+ memoryWarningObserver = NotificationCenter.default.addObserver(
17
19
  forName: UIApplication.didReceiveMemoryWarningNotification,
18
20
  object: nil,
19
21
  queue: .main
20
- ) { [weak self] _ in
21
- self?.onLog?("warning", "Memory warning received, cleaning DNS cache")
22
+ ) { _ in
23
+ SniConnectLog.info("Memory warning received, cleaning DNS cache")
22
24
  DNSResolver.cleanExpiredEntries()
23
25
  }
24
26
  }
25
27
 
26
28
  deinit {
27
- NotificationCenter.default.removeObserver(self)
29
+ if let observer = memoryWarningObserver {
30
+ NotificationCenter.default.removeObserver(observer)
31
+ }
28
32
  }
29
33
 
30
34
  struct RequestConfig {
@@ -164,8 +168,10 @@ final class SniConnectClient {
164
168
  curlConfig.enableBuiltInRedirection = true
165
169
  curlConfig.cacheEnabled = false
166
170
 
167
- // Enable full certificate validation for security
168
- // The certificate will be validated against the SNI hostname, not the IP
171
+ // Enable full certificate validation for security.
172
+ // The certificate is validated against the SNI hostname, not the IP, because
173
+ // the custom DNS resolver only overrides address resolution — libcurl keeps the
174
+ // original hostname for SNI and certificate CN/SAN matching.
169
175
  curlConfig.certificateValidationEnabled = true
170
176
  curlConfig.domainNameVerificationEnabled = true
171
177
  curlConfig.dnsResolver = DNSResolver.self
@@ -178,88 +184,53 @@ final class SniConnectClient {
178
184
  private static let queue = DispatchQueue(label: "com.onekey.sni.connect.dns", attributes: .concurrent)
179
185
  private static let cache = DNSCache()
180
186
 
181
- /// LRU cache for DNS mappings with size limit and TTL support
182
- /// Uses hostname:IP as cache key to ensure different IPs for the same hostname are isolated
183
- private class DNSCache {
184
- private struct CacheEntry {
185
- let hostname: String
187
+ /// Thread-safe hostname -> IP pin with TTL.
188
+ ///
189
+ /// LIMITATION: EMASCurl only exposes a process-global DNS resolver
190
+ /// (`setDNSResolver:`), which receives only the hostname. There is no
191
+ /// per-request DNS API, so the pin is keyed by hostname and the most recent
192
+ /// IP for a hostname wins. Concurrent requests to the SAME hostname targeting
193
+ /// DIFFERENT IPs are therefore not guaranteed to each hit their own IP. Normal
194
+ /// usage (one IP per hostname at a time) is unaffected.
195
+ private final class DNSCache {
196
+ private struct Entry {
186
197
  let ip: String
187
198
  let timestamp: TimeInterval
188
199
  }
189
200
 
190
- private var storage: [String: CacheEntry] = [:]
191
- private var accessOrder: [String] = []
192
-
193
- // Mapping from hostname to IP for DNS resolution
194
- // This stores the most recently set IP for each hostname
195
- private var hostnameToIP: [String: String] = [:]
196
-
197
- // Maximum cache entries (100 as specified in requirements)
201
+ private var hostnameToEntry: [String: Entry] = [:]
198
202
  private let maxSize = 100
199
- // TTL in seconds (5 minutes default)
200
- private let ttl: TimeInterval = 300
201
-
202
- /// Generate cache key using hostname:IP format
203
- /// This ensures different IPs for the same hostname are isolated (accurate speed testing)
204
- private func makeCacheKey(hostname: String, ip: String) -> String {
205
- return "\(hostname.lowercased()):\(ip)"
206
- }
203
+ private let ttl: TimeInterval = 300 // 5 minutes
207
204
 
208
205
  func get(_ domain: String) -> String? {
209
- let normalizedDomain = domain.lowercased()
210
-
211
- // Return the most recently set IP for this hostname
212
- return hostnameToIP[normalizedDomain]
206
+ let key = domain.lowercased()
207
+ guard let entry = hostnameToEntry[key] else { return nil }
208
+ if Date().timeIntervalSince1970 - entry.timestamp > ttl {
209
+ return nil
210
+ }
211
+ return entry.ip
213
212
  }
214
213
 
215
214
  func set(_ ip: String, for domain: String) {
216
- let normalizedDomain = domain.lowercased()
217
- let key = makeCacheKey(hostname: normalizedDomain, ip: ip)
218
-
219
- // Update hostname to IP mapping
220
- hostnameToIP[normalizedDomain] = ip
221
-
222
- // Evict oldest entry if cache is full
223
- if storage.count >= maxSize && storage[key] == nil {
224
- if let oldest = accessOrder.first {
225
- storage.removeValue(forKey: oldest)
226
- accessOrder.removeFirst()
215
+ let key = domain.lowercased()
216
+ // Evict the oldest entry when at capacity (and this is a new host).
217
+ if hostnameToEntry.count >= maxSize && hostnameToEntry[key] == nil {
218
+ if let oldest = hostnameToEntry.min(by: { $0.value.timestamp < $1.value.timestamp })?.key {
219
+ hostnameToEntry.removeValue(forKey: oldest)
227
220
  }
228
221
  }
229
-
230
- // Add or update entry
231
- let entry = CacheEntry(hostname: normalizedDomain, ip: ip, timestamp: Date().timeIntervalSince1970)
232
- storage[key] = entry
233
-
234
- // Update access order
235
- if let index = accessOrder.firstIndex(of: key) {
236
- accessOrder.remove(at: index)
237
- }
238
- accessOrder.append(key)
239
- }
240
-
241
- func remove(_ key: String) {
242
- storage.removeValue(forKey: key)
243
- if let index = accessOrder.firstIndex(of: key) {
244
- accessOrder.remove(at: index)
245
- }
222
+ hostnameToEntry[key] = Entry(ip: ip, timestamp: Date().timeIntervalSince1970)
246
223
  }
247
224
 
248
225
  func clear() {
249
- storage.removeAll()
250
- accessOrder.removeAll()
251
- hostnameToIP.removeAll()
226
+ hostnameToEntry.removeAll()
252
227
  }
253
228
 
254
229
  func cleanExpired() {
255
230
  let now = Date().timeIntervalSince1970
256
- let expiredKeys = storage.filter { now - $0.value.timestamp > ttl }.map { $0.key }
257
- for key in expiredKeys {
258
- remove(key)
259
- // Also remove from hostnameToIP if this was the active mapping
260
- if let entry = storage[key], hostnameToIP[entry.hostname] == entry.ip {
261
- hostnameToIP.removeValue(forKey: entry.hostname)
262
- }
231
+ let expired = hostnameToEntry.filter { now - $0.value.timestamp > ttl }.map { $0.key }
232
+ for key in expired {
233
+ hostnameToEntry.removeValue(forKey: key)
263
234
  }
264
235
  }
265
236
  }
@@ -296,19 +267,19 @@ final class SniConnectClient {
296
267
  /// Clear all DNS cache entries
297
268
  func clearDNSCache() {
298
269
  DNSResolver.clearCache()
299
- onLog?("info", "DNS cache cleared")
270
+ SniConnectLog.info("DNS cache cleared")
300
271
  }
301
272
 
302
273
  /// Cancel a request by ID
303
274
  func cancelRequest(requestId: String) {
304
275
  tasksQueue.async(flags: .barrier) { [weak self] in
305
276
  guard let task = self?.activeTasks[requestId] else {
306
- self?.onLog?("warning", "No active request found with ID: \(requestId)")
277
+ SniConnectLog.warn("No active request found with ID: \(requestId)")
307
278
  return
308
279
  }
309
280
  task.cancel()
310
281
  self?.activeTasks.removeValue(forKey: requestId)
311
- self?.onLog?("info", "Request cancelled: \(requestId)")
282
+ SniConnectLog.info("Request cancelled: \(requestId)")
312
283
  }
313
284
  }
314
285
 
@@ -321,15 +292,14 @@ final class SniConnectClient {
321
292
  task.cancel()
322
293
  }
323
294
  self.activeTasks.removeAll()
324
- self.onLog?("info", "Cancelled \(count) active requests")
295
+ SniConnectLog.info("Cancelled \(count) active requests")
325
296
  }
326
297
  }
327
298
 
328
- /// Register an active task (synchronous for immediate registration)
299
+ /// Register an active task immediately after creation, before JS can cancel it.
329
300
  func registerTask(_ task: Task<Response, Error>, for requestId: String) {
330
301
  tasksQueue.sync(flags: .barrier) { [weak self] in
331
302
  self?.activeTasks[requestId] = task
332
- self?.onLog?("info", "Registered request: \(requestId), total active: \(self?.activeTasks.count ?? 0)")
333
303
  }
334
304
  }
335
305
 
@@ -338,7 +308,6 @@ final class SniConnectClient {
338
308
  guard let requestId = requestId else { return }
339
309
  tasksQueue.async(flags: .barrier) { [weak self] in
340
310
  self?.activeTasks.removeValue(forKey: requestId)
341
- self?.onLog?("info", "Unregistered request: \(requestId)")
342
311
  }
343
312
  }
344
313
 
@@ -350,12 +319,25 @@ final class SniConnectClient {
350
319
  unregisterTask(requestId: config.requestId)
351
320
  }
352
321
 
322
+ // Validate every caller-controlled field before it reaches the network layer.
323
+ let method: String
324
+ let normalizedPath: String
325
+ do {
326
+ try SniConnectValidation.validatePublicIP(config.ip)
327
+ try SniConnectValidation.validateHostname(config.hostname)
328
+ try SniConnectValidation.validateHeaders(config.headers)
329
+ method = try SniConnectValidation.normalizeMethod(config.method)
330
+ normalizedPath = try SniConnectValidation.normalizePath(config.path)
331
+ } catch {
332
+ throw SniConnectError.invalidConfig("\(error)")
333
+ }
334
+
353
335
  DNSResolver.setIP(config.ip, for: config.hostname)
354
336
 
355
- let url = try Self.buildURL(hostname: config.hostname, path: config.path)
337
+ let url = try Self.buildURL(hostname: config.hostname, normalizedPath: normalizedPath)
356
338
 
357
339
  let mutableRequest = NSMutableURLRequest(url: url)
358
- mutableRequest.httpMethod = config.method.uppercased()
340
+ mutableRequest.httpMethod = method
359
341
 
360
342
  // Convert milliseconds to seconds for timeout values
361
343
  let totalTimeoutSeconds = config.effectiveTotalTimeout / 1000.0
@@ -380,15 +362,15 @@ final class SniConnectClient {
380
362
  mutableRequest.httpBody = bodyData
381
363
  }
382
364
 
383
- // Configure connection timeout for EMASCurl
384
- EMASCurlProtocol.setConnectTimeoutInterval(connectTimeoutSeconds)
365
+ // Per-request connect timeout (avoids the process-global setter race).
366
+ EMASCurlProtocol.setConnectTimeoutIntervalFor(mutableRequest, connectTimeoutInterval: connectTimeoutSeconds)
385
367
  let request = mutableRequest as URLRequest
386
368
 
387
369
  do {
388
370
  let (data, response) = try await Self.urlSession.data(for: request)
389
371
  guard let httpResponse = response as? HTTPURLResponse else {
390
372
  let errorMsg = "Invalid HTTP response type"
391
- onLog?("error", errorMsg)
373
+ SniConnectLog.error(errorMsg)
392
374
  throw SniConnectError.invalidConfig(errorMsg)
393
375
  }
394
376
 
@@ -397,11 +379,10 @@ final class SniConnectClient {
397
379
  let (headers, multiValueHeaders) = Self.extractHeaders(from: httpResponse)
398
380
  let statusText = HTTPURLResponse.localizedString(forStatusCode: status)
399
381
 
400
- // Check for HTTP errors (4xx, 5xx)
382
+ // 4xx/5xx are returned to JS as a normal response (the caller inspects
383
+ // `status`); we only record it for diagnostics.
401
384
  if status >= 400 {
402
- let error = SniConnectError.httpError(code: status, message: statusText)
403
- onLog?("error", "HTTP error: \(error.message)")
404
- // Still return the response for client to handle
385
+ SniConnectLog.warn("HTTP \(status) for \(config.hostname)")
405
386
  }
406
387
 
407
388
  return Response(
@@ -412,33 +393,33 @@ final class SniConnectClient {
412
393
  multiValueHeaders: multiValueHeaders
413
394
  )
414
395
  } catch let error as SniConnectError {
415
- onLog?("error", "[\(error.code)] \(error.message)")
396
+ SniConnectLog.error("[\(error.code)] \(error.message)")
416
397
  throw error
417
398
  } catch {
418
399
  // Convert generic errors to specific SniConnectError types
419
400
  let sniError = SniConnectError.from(error)
420
- onLog?("error", "[\(sniError.code)] \(sniError.message)")
401
+ SniConnectLog.error("[\(sniError.code)] \(sniError.message)")
421
402
  throw sniError
422
403
  }
423
404
  }
424
405
 
425
- private static func buildURL(hostname: String, path: String) throws -> URL {
426
- let trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines)
427
-
428
- if let url = URL(string: trimmedPath), url.scheme != nil {
429
- return url
430
- }
431
-
432
- let normalizedPath: String
433
- if trimmedPath.isEmpty {
434
- normalizedPath = "/"
435
- } else if trimmedPath.hasPrefix("/") {
436
- normalizedPath = trimmedPath
406
+ /// Build the request URL. Always `https://<hostname><path>` on port 443 the
407
+ /// path has already been validated as relative (no scheme/authority), so the
408
+ /// caller cannot override scheme, host or port.
409
+ private static func buildURL(hostname: String, normalizedPath: String) throws -> URL {
410
+ var components = URLComponents()
411
+ components.scheme = "https"
412
+ components.host = hostname
413
+ // `normalizedPath` is "/...optional?query". Split off the query so URLComponents
414
+ // percent-encodes each part correctly.
415
+ if let queryIndex = normalizedPath.firstIndex(of: "?") {
416
+ components.percentEncodedPath = String(normalizedPath[..<queryIndex])
417
+ let queryStart = normalizedPath.index(after: queryIndex)
418
+ components.percentEncodedQuery = String(normalizedPath[queryStart...])
437
419
  } else {
438
- normalizedPath = "/" + trimmedPath
420
+ components.percentEncodedPath = normalizedPath
439
421
  }
440
-
441
- guard let url = URL(string: "https://\(hostname)\(normalizedPath)") else {
422
+ guard let url = components.url else {
442
423
  throw SniConnectError.invalidURL
443
424
  }
444
425
  return url
@@ -446,7 +427,7 @@ final class SniConnectClient {
446
427
 
447
428
  private static func parseResponseData(_ data: Data) -> Any {
448
429
  guard !data.isEmpty else {
449
- return [:]
430
+ return ""
450
431
  }
451
432
 
452
433
  if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
@@ -483,13 +464,7 @@ final class SniConnectClient {
483
464
  for (key, values) in headerGroups {
484
465
  // For backward compatibility, single-value headers use the last value
485
466
  singleValueHeaders[key] = values.last
486
-
487
- // Multi-value headers contain all values
488
- if values.count > 1 {
489
- multiValueHeaders[key] = values
490
- } else {
491
- multiValueHeaders[key] = values
492
- }
467
+ multiValueHeaders[key] = values
493
468
  }
494
469
 
495
470
  return (singleValueHeaders, multiValueHeaders)
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+
3
+ /// Lightweight logging wrapper that dynamically dispatches to OneKeyLog through
4
+ /// the Objective-C runtime.
5
+ ///
6
+ /// Using reflection (instead of `import ReactNativeNativeLogger`) keeps this
7
+ /// TurboModule from hard-linking the nitro-based native-logger module. When
8
+ /// OneKeyLog is unavailable the logs are silently dropped. Mirrors the Android
9
+ /// `SniConnectLogger` and the existing `BTLogger` / `SBLLogger`.
10
+ enum SniConnectLog {
11
+ private static let tag = "SniConnect"
12
+
13
+ // Swift classes are exposed to the ObjC runtime as `Module.ClassName`.
14
+ private static let logClass: AnyObject? =
15
+ (NSClassFromString("ReactNativeNativeLogger.OneKeyLog")
16
+ ?? NSClassFromString("OneKeyLog")) as AnyObject?
17
+
18
+ static func debug(_ message: String) { dispatch("debug::", message) }
19
+ static func info(_ message: String) { dispatch("info::", message) }
20
+ static func warn(_ message: String) { dispatch("warn::", message) }
21
+ static func error(_ message: String) { dispatch("error::", message) }
22
+
23
+ private static func dispatch(_ selectorName: String, _ message: String) {
24
+ guard let cls = logClass else { return }
25
+ let sel = NSSelectorFromString(selectorName)
26
+ guard cls.responds(to: sel) else { return }
27
+ _ = cls.perform(sel, with: tag, with: message)
28
+ }
29
+ }
@@ -0,0 +1,151 @@
1
+ import Foundation
2
+
3
+ /// Boundary validation/normalization for SNI request inputs.
4
+ ///
5
+ /// The module connects to a caller-supplied IP while preserving the TLS SNI/Host
6
+ /// of `hostname`. Because the connect target is caller-controlled, every field that
7
+ /// reaches the network layer is validated here to prevent SSRF, scheme/host/port
8
+ /// override, cleartext downgrade and CR/LF header injection.
9
+ enum SniConnectValidation {
10
+
11
+ enum ValidationError: Error {
12
+ case invalidIP(String)
13
+ case forbiddenIP(String)
14
+ case invalidHostname(String)
15
+ case invalidMethod(String)
16
+ case invalidPath(String)
17
+ case invalidHeader(String)
18
+ }
19
+
20
+ /// HTTP methods the module is allowed to issue.
21
+ private static let allowedMethods: Set<String> = [
22
+ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS",
23
+ ]
24
+
25
+ /// Validate and uppercase the HTTP method.
26
+ static func normalizeMethod(_ method: String) throws -> String {
27
+ let upper = method.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
28
+ guard allowedMethods.contains(upper) else {
29
+ throw ValidationError.invalidMethod(method)
30
+ }
31
+ return upper
32
+ }
33
+
34
+ /// Validate `hostname` as a DNS host (used for SNI, Host header and cert matching).
35
+ static func validateHostname(_ hostname: String) throws {
36
+ guard hostname.count <= 253, !hostname.isEmpty else {
37
+ throw ValidationError.invalidHostname(hostname)
38
+ }
39
+ // Labels: 1-63 chars, alphanumeric + hyphen, not starting/ending with hyphen.
40
+ let pattern = "^(?=.{1,253}$)([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$"
41
+ guard hostname.range(of: pattern, options: .regularExpression) != nil else {
42
+ throw ValidationError.invalidHostname(hostname)
43
+ }
44
+ }
45
+
46
+ /// Validate `path`: must be a relative path/query only. Reject absolute URLs
47
+ /// (scheme/authority), protocol-relative URLs and control characters so the
48
+ /// caller cannot override scheme/host/port or downgrade to cleartext.
49
+ static func normalizePath(_ path: String) throws -> String {
50
+ let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
51
+ if containsControlCharacters(trimmed) {
52
+ throw ValidationError.invalidPath(path)
53
+ }
54
+ // Reject anything that looks like it carries a scheme or authority.
55
+ if trimmed.contains("://") || trimmed.hasPrefix("//") {
56
+ throw ValidationError.invalidPath(path)
57
+ }
58
+ // A bare scheme like "javascript:..." has no "//"; reject any leading scheme.
59
+ if let schemeRange = trimmed.range(of: "^[A-Za-z][A-Za-z0-9+.-]*:", options: .regularExpression),
60
+ schemeRange.lowerBound == trimmed.startIndex {
61
+ throw ValidationError.invalidPath(path)
62
+ }
63
+ if trimmed.isEmpty { return "/" }
64
+ return trimmed.hasPrefix("/") ? trimmed : "/" + trimmed
65
+ }
66
+
67
+ /// Validate header names/values: reject CR/LF/control characters (header
68
+ /// injection) and the Host header (the module sets Host itself).
69
+ static func validateHeaders(_ headers: [String: String]) throws {
70
+ for (key, value) in headers {
71
+ if key.isEmpty || containsControlCharacters(key) || containsControlCharacters(value) {
72
+ throw ValidationError.invalidHeader(key)
73
+ }
74
+ }
75
+ }
76
+
77
+ static func containsControlCharacters(_ s: String) -> Bool {
78
+ return s.unicodeScalars.contains { $0.value < 0x20 || $0.value == 0x7F }
79
+ }
80
+
81
+ // MARK: - IP validation
82
+
83
+ /// Validate `ip` is a literal IPv4/IPv6 address (never a hostname) and routes
84
+ /// to a public/global-unicast destination. Rejects loopback, private,
85
+ /// link-local (incl. 169.254.169.254 metadata), CGNAT, multicast and reserved.
86
+ static func validatePublicIP(_ ip: String) throws {
87
+ if let v4 = parseIPv4(ip) {
88
+ if isForbiddenIPv4(v4) { throw ValidationError.forbiddenIP(ip) }
89
+ return
90
+ }
91
+ if let v6 = parseIPv6(ip) {
92
+ if isForbiddenIPv6(v6) { throw ValidationError.forbiddenIP(ip) }
93
+ return
94
+ }
95
+ throw ValidationError.invalidIP(ip)
96
+ }
97
+
98
+ private static func parseIPv4(_ ip: String) -> [UInt8]? {
99
+ var addr = in_addr()
100
+ guard ip.withCString({ inet_pton(AF_INET, $0, &addr) }) == 1 else { return nil }
101
+ let raw = addr.s_addr.bigEndian
102
+ return [
103
+ UInt8((raw >> 24) & 0xFF),
104
+ UInt8((raw >> 16) & 0xFF),
105
+ UInt8((raw >> 8) & 0xFF),
106
+ UInt8(raw & 0xFF),
107
+ ]
108
+ }
109
+
110
+ private static func parseIPv6(_ ip: String) -> [UInt8]? {
111
+ var addr = in6_addr()
112
+ guard ip.withCString({ inet_pton(AF_INET6, $0, &addr) }) == 1 else { return nil }
113
+ return withUnsafeBytes(of: &addr) { Array($0.bindMemory(to: UInt8.self)) }
114
+ }
115
+
116
+ private static func isForbiddenIPv4(_ b: [UInt8]) -> Bool {
117
+ let a = b[0], c = b[1], d = b[2]
118
+ if a == 0 { return true } // 0.0.0.0/8 "this network"
119
+ if a == 10 { return true } // 10/8 private
120
+ if a == 127 { return true } // 127/8 loopback
121
+ if a == 100 && (c & 0xC0) == 0x40 { return true } // 100.64/10 CGNAT
122
+ if a == 169 && c == 254 { return true } // 169.254/16 link-local + metadata
123
+ if a == 172 && c >= 16 && c <= 31 { return true } // 172.16/12 private
124
+ if a == 192 && c == 168 { return true } // 192.168/16 private
125
+ if a == 192 && c == 0 && d == 0 { return true } // 192.0.0/24
126
+ if a == 192 && c == 0 && d == 2 { return true } // 192.0.2/24 TEST-NET-1
127
+ if a == 198 && (c == 18 || c == 19) { return true } // 198.18/15 benchmarking
128
+ if a == 198 && c == 51 && d == 100 { return true } // 198.51.100/24 TEST-NET-2
129
+ if a == 203 && c == 0 && d == 113 { return true } // 203.0.113/24 TEST-NET-3
130
+ if a >= 224 { return true } // 224/4 multicast + 240/4 reserved + broadcast
131
+ return false
132
+ }
133
+
134
+ private static func isForbiddenIPv6(_ b: [UInt8]) -> Bool {
135
+ // Unspecified ::
136
+ if b.allSatisfy({ $0 == 0 }) { return true }
137
+ // Loopback ::1
138
+ if b[0...14].allSatisfy({ $0 == 0 }) && b[15] == 1 { return true }
139
+ // Multicast ff00::/8
140
+ if b[0] == 0xFF { return true }
141
+ // Link-local fe80::/10
142
+ if b[0] == 0xFE && (b[1] & 0xC0) == 0x80 { return true }
143
+ // Unique local fc00::/7
144
+ if (b[0] & 0xFE) == 0xFC { return true }
145
+ // IPv4-mapped ::ffff:0:0/96 — validate the embedded IPv4
146
+ if b[0...9].allSatisfy({ $0 == 0 }) && b[10] == 0xFF && b[11] == 0xFF {
147
+ return isForbiddenIPv4([b[12], b[13], b[14], b[15]])
148
+ }
149
+ return false
150
+ }
151
+ }
@@ -1,21 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { NativeEventEmitter } from 'react-native';
4
3
  import NativeSniConnect from "./NativeSniConnect.js";
5
-
6
- // Log entry type definition
7
-
8
- // Create event emitter instance
9
- const eventEmitter = new NativeEventEmitter(NativeSniConnect);
10
-
11
- // Simple log subscription function
12
- export function subscribeToLogs(callback) {
13
- const subscription = eventEmitter.addListener('SniConnectLog', log => {
14
- // Type assertion since we know the structure
15
- callback(log);
16
- });
17
- return () => subscription.remove();
18
- }
19
4
  export function request(config) {
20
5
  return NativeSniConnect.request(config);
21
6
  }
@@ -30,21 +30,8 @@ export interface Spec extends TurboModule {
30
30
  clearDNSCache(): Promise<{
31
31
  success: boolean;
32
32
  }>;
33
- addListener(eventType: string): void;
34
- removeListeners(count: Int32): void;
35
33
  }
36
- export type SniConnectModule = Spec & {
37
- request(config: SniConnectRequest): Promise<SniConnectResponse>;
38
- cancelRequest(requestId: string): Promise<{
39
- success: boolean;
40
- }>;
41
- cancelAllRequests(): Promise<{
42
- success: boolean;
43
- }>;
44
- clearDNSCache(): Promise<{
45
- success: boolean;
46
- }>;
47
- };
34
+ export type SniConnectModule = Spec;
48
35
  declare const NativeSniConnect: SniConnectModule;
49
36
  export default NativeSniConnect;
50
37
  //# sourceMappingURL=NativeSniConnect.d.ts.map
@@ -1,10 +1,4 @@
1
1
  import { type SniConnectRequest, type SniConnectResponse } from './NativeSniConnect';
2
- export type LogEntry = {
3
- level: string;
4
- message: string;
5
- timestamp: number;
6
- };
7
- export declare function subscribeToLogs(callback: (log: LogEntry) => void): () => void;
8
2
  export declare function request(config: SniConnectRequest): Promise<SniConnectResponse>;
9
3
  export declare function cancelRequest(requestId: string): Promise<{
10
4
  success: boolean;