@onekeyfe/react-native-sni-connect 1.0.0 → 3.0.69

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,81 +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
- private class DNSCache {
183
- private struct CacheEntry {
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 {
184
197
  let ip: String
185
198
  let timestamp: TimeInterval
186
199
  }
187
200
 
188
- private var storage: [String: CacheEntry] = [:]
189
- private var accessOrder: [String] = []
190
-
191
- // Maximum cache entries (100 as specified in requirements)
201
+ private var hostnameToEntry: [String: Entry] = [:]
192
202
  private let maxSize = 100
193
- // TTL in seconds (5 minutes default)
194
- private let ttl: TimeInterval = 300
203
+ private let ttl: TimeInterval = 300 // 5 minutes
195
204
 
196
205
  func get(_ domain: String) -> String? {
197
206
  let key = domain.lowercased()
198
- guard let entry = storage[key] else { return nil }
199
-
200
- // Check if entry has expired
201
- let now = Date().timeIntervalSince1970
202
- if now - entry.timestamp > ttl {
203
- remove(key)
207
+ guard let entry = hostnameToEntry[key] else { return nil }
208
+ if Date().timeIntervalSince1970 - entry.timestamp > ttl {
204
209
  return nil
205
210
  }
206
-
207
- // Update access order (LRU)
208
- if let index = accessOrder.firstIndex(of: key) {
209
- accessOrder.remove(at: index)
210
- }
211
- accessOrder.append(key)
212
-
213
211
  return entry.ip
214
212
  }
215
213
 
216
214
  func set(_ ip: String, for domain: String) {
217
215
  let key = domain.lowercased()
218
-
219
- // Evict oldest entry if cache is full
220
- if storage.count >= maxSize && storage[key] == nil {
221
- if let oldest = accessOrder.first {
222
- storage.removeValue(forKey: oldest)
223
- accessOrder.removeFirst()
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)
224
220
  }
225
221
  }
226
-
227
- // Add or update entry
228
- let entry = CacheEntry(ip: ip, timestamp: Date().timeIntervalSince1970)
229
- storage[key] = entry
230
-
231
- // Update access order
232
- if let index = accessOrder.firstIndex(of: key) {
233
- accessOrder.remove(at: index)
234
- }
235
- accessOrder.append(key)
236
- }
237
-
238
- func remove(_ domain: String) {
239
- let key = domain.lowercased()
240
- storage.removeValue(forKey: key)
241
- if let index = accessOrder.firstIndex(of: key) {
242
- accessOrder.remove(at: index)
243
- }
222
+ hostnameToEntry[key] = Entry(ip: ip, timestamp: Date().timeIntervalSince1970)
244
223
  }
245
224
 
246
225
  func clear() {
247
- storage.removeAll()
248
- accessOrder.removeAll()
226
+ hostnameToEntry.removeAll()
249
227
  }
250
228
 
251
229
  func cleanExpired() {
252
230
  let now = Date().timeIntervalSince1970
253
- let expiredKeys = storage.filter { now - $0.value.timestamp > ttl }.map { $0.key }
254
- for key in expiredKeys {
255
- remove(key)
231
+ let expired = hostnameToEntry.filter { now - $0.value.timestamp > ttl }.map { $0.key }
232
+ for key in expired {
233
+ hostnameToEntry.removeValue(forKey: key)
256
234
  }
257
235
  }
258
236
  }
@@ -289,19 +267,19 @@ final class SniConnectClient {
289
267
  /// Clear all DNS cache entries
290
268
  func clearDNSCache() {
291
269
  DNSResolver.clearCache()
292
- onLog?("info", "DNS cache cleared")
270
+ SniConnectLog.info("DNS cache cleared")
293
271
  }
294
272
 
295
273
  /// Cancel a request by ID
296
274
  func cancelRequest(requestId: String) {
297
275
  tasksQueue.async(flags: .barrier) { [weak self] in
298
276
  guard let task = self?.activeTasks[requestId] else {
299
- self?.onLog?("warning", "No active request found with ID: \(requestId)")
277
+ SniConnectLog.warn("No active request found with ID: \(requestId)")
300
278
  return
301
279
  }
302
280
  task.cancel()
303
281
  self?.activeTasks.removeValue(forKey: requestId)
304
- self?.onLog?("info", "Request cancelled: \(requestId)")
282
+ SniConnectLog.info("Request cancelled: \(requestId)")
305
283
  }
306
284
  }
307
285
 
@@ -314,15 +292,14 @@ final class SniConnectClient {
314
292
  task.cancel()
315
293
  }
316
294
  self.activeTasks.removeAll()
317
- self.onLog?("info", "Cancelled \(count) active requests")
295
+ SniConnectLog.info("Cancelled \(count) active requests")
318
296
  }
319
297
  }
320
298
 
321
- /// Register an active task (synchronous for immediate registration)
299
+ /// Register an active task immediately after creation, before JS can cancel it.
322
300
  func registerTask(_ task: Task<Response, Error>, for requestId: String) {
323
301
  tasksQueue.sync(flags: .barrier) { [weak self] in
324
302
  self?.activeTasks[requestId] = task
325
- self?.onLog?("info", "Registered request: \(requestId), total active: \(self?.activeTasks.count ?? 0)")
326
303
  }
327
304
  }
328
305
 
@@ -331,7 +308,6 @@ final class SniConnectClient {
331
308
  guard let requestId = requestId else { return }
332
309
  tasksQueue.async(flags: .barrier) { [weak self] in
333
310
  self?.activeTasks.removeValue(forKey: requestId)
334
- self?.onLog?("info", "Unregistered request: \(requestId)")
335
311
  }
336
312
  }
337
313
 
@@ -343,12 +319,25 @@ final class SniConnectClient {
343
319
  unregisterTask(requestId: config.requestId)
344
320
  }
345
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
+
346
335
  DNSResolver.setIP(config.ip, for: config.hostname)
347
336
 
348
- let url = try Self.buildURL(hostname: config.hostname, path: config.path)
337
+ let url = try Self.buildURL(hostname: config.hostname, normalizedPath: normalizedPath)
349
338
 
350
339
  let mutableRequest = NSMutableURLRequest(url: url)
351
- mutableRequest.httpMethod = config.method.uppercased()
340
+ mutableRequest.httpMethod = method
352
341
 
353
342
  // Convert milliseconds to seconds for timeout values
354
343
  let totalTimeoutSeconds = config.effectiveTotalTimeout / 1000.0
@@ -373,15 +362,15 @@ final class SniConnectClient {
373
362
  mutableRequest.httpBody = bodyData
374
363
  }
375
364
 
376
- // Configure connection timeout for EMASCurl
377
- EMASCurlProtocol.setConnectTimeoutInterval(connectTimeoutSeconds)
365
+ // Per-request connect timeout (avoids the process-global setter race).
366
+ EMASCurlProtocol.setConnectTimeoutIntervalFor(mutableRequest, connectTimeoutInterval: connectTimeoutSeconds)
378
367
  let request = mutableRequest as URLRequest
379
368
 
380
369
  do {
381
370
  let (data, response) = try await Self.urlSession.data(for: request)
382
371
  guard let httpResponse = response as? HTTPURLResponse else {
383
372
  let errorMsg = "Invalid HTTP response type"
384
- onLog?("error", errorMsg)
373
+ SniConnectLog.error(errorMsg)
385
374
  throw SniConnectError.invalidConfig(errorMsg)
386
375
  }
387
376
 
@@ -390,11 +379,10 @@ final class SniConnectClient {
390
379
  let (headers, multiValueHeaders) = Self.extractHeaders(from: httpResponse)
391
380
  let statusText = HTTPURLResponse.localizedString(forStatusCode: status)
392
381
 
393
- // 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.
394
384
  if status >= 400 {
395
- let error = SniConnectError.httpError(code: status, message: statusText)
396
- onLog?("error", "HTTP error: \(error.message)")
397
- // Still return the response for client to handle
385
+ SniConnectLog.warn("HTTP \(status) for \(config.hostname)")
398
386
  }
399
387
 
400
388
  return Response(
@@ -405,33 +393,33 @@ final class SniConnectClient {
405
393
  multiValueHeaders: multiValueHeaders
406
394
  )
407
395
  } catch let error as SniConnectError {
408
- onLog?("error", "[\(error.code)] \(error.message)")
396
+ SniConnectLog.error("[\(error.code)] \(error.message)")
409
397
  throw error
410
398
  } catch {
411
399
  // Convert generic errors to specific SniConnectError types
412
400
  let sniError = SniConnectError.from(error)
413
- onLog?("error", "[\(sniError.code)] \(sniError.message)")
401
+ SniConnectLog.error("[\(sniError.code)] \(sniError.message)")
414
402
  throw sniError
415
403
  }
416
404
  }
417
405
 
418
- private static func buildURL(hostname: String, path: String) throws -> URL {
419
- let trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines)
420
-
421
- if let url = URL(string: trimmedPath), url.scheme != nil {
422
- return url
423
- }
424
-
425
- let normalizedPath: String
426
- if trimmedPath.isEmpty {
427
- normalizedPath = "/"
428
- } else if trimmedPath.hasPrefix("/") {
429
- 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...])
430
419
  } else {
431
- normalizedPath = "/" + trimmedPath
420
+ components.percentEncodedPath = normalizedPath
432
421
  }
433
-
434
- guard let url = URL(string: "https://\(hostname)\(normalizedPath)") else {
422
+ guard let url = components.url else {
435
423
  throw SniConnectError.invalidURL
436
424
  }
437
425
  return url
@@ -439,7 +427,7 @@ final class SniConnectClient {
439
427
 
440
428
  private static func parseResponseData(_ data: Data) -> Any {
441
429
  guard !data.isEmpty else {
442
- return [:]
430
+ return ""
443
431
  }
444
432
 
445
433
  if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
@@ -476,13 +464,7 @@ final class SniConnectClient {
476
464
  for (key, values) in headerGroups {
477
465
  // For backward compatibility, single-value headers use the last value
478
466
  singleValueHeaders[key] = values.last
479
-
480
- // Multi-value headers contain all values
481
- if values.count > 1 {
482
- multiValueHeaders[key] = values
483
- } else {
484
- multiValueHeaders[key] = values
485
- }
467
+ multiValueHeaders[key] = values
486
468
  }
487
469
 
488
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;