@onekeyfe/react-native-sni-connect 1.0.0
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/LICENSE +20 -0
- package/README.md +37 -0
- package/SniConnect.podspec +36 -0
- package/android/build.gradle +89 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +346 -0
- package/android/src/main/java/com/sniconnect/SniConnectPackage.kt +31 -0
- package/ios/SniConnect-Bridging-Header.h +13 -0
- package/ios/SniConnect.mm +141 -0
- package/ios/SniConnect.swift +185 -0
- package/ios/SniConnectClient.swift +490 -0
- package/lib/module/@types/react-native-codegen.d.js +2 -0
- package/lib/module/@types/react-native-codegen.d.js.map +1 -0
- package/lib/module/NativeSniConnect.js +17 -0
- package/lib/module/NativeSniConnect.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSniConnect.d.ts +50 -0
- package/lib/typescript/src/NativeSniConnect.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +167 -0
- package/src/@types/react-native-codegen.d.ts +8 -0
- package/src/NativeSniConnect.ts +70 -0
- package/src/index.tsx +46 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import EMASCurl
|
|
3
|
+
|
|
4
|
+
/// Core HTTPS client that enforces IP direct connection with SNI.
|
|
5
|
+
final class SniConnectClient {
|
|
6
|
+
|
|
7
|
+
// Logging callback
|
|
8
|
+
var onLog: ((String, String) -> Void)?
|
|
9
|
+
|
|
10
|
+
// Active requests tracking for cancellation support
|
|
11
|
+
private var activeTasks: [String: Task<Response, Error>] = [:]
|
|
12
|
+
private let tasksQueue = DispatchQueue(label: "com.onekey.sni.connect.tasks", attributes: .concurrent)
|
|
13
|
+
|
|
14
|
+
init() {
|
|
15
|
+
// Register for memory warnings to clean cache
|
|
16
|
+
NotificationCenter.default.addObserver(
|
|
17
|
+
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
18
|
+
object: nil,
|
|
19
|
+
queue: .main
|
|
20
|
+
) { [weak self] _ in
|
|
21
|
+
self?.onLog?("warning", "Memory warning received, cleaning DNS cache")
|
|
22
|
+
DNSResolver.cleanExpiredEntries()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
deinit {
|
|
27
|
+
NotificationCenter.default.removeObserver(self)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
struct RequestConfig {
|
|
31
|
+
let requestId: String? // Optional request ID for cancellation
|
|
32
|
+
let ip: String
|
|
33
|
+
let hostname: String
|
|
34
|
+
let method: String
|
|
35
|
+
let path: String
|
|
36
|
+
let headers: [String: String]
|
|
37
|
+
let body: String?
|
|
38
|
+
let timeout: TimeInterval
|
|
39
|
+
|
|
40
|
+
// Advanced timeout configurations
|
|
41
|
+
let connectTimeout: TimeInterval? // Connection establishment timeout
|
|
42
|
+
let totalTimeout: TimeInterval? // Total request timeout (overrides `timeout`)
|
|
43
|
+
|
|
44
|
+
var effectiveConnectTimeout: TimeInterval {
|
|
45
|
+
connectTimeout ?? min(timeout / 3, 10.0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var effectiveTotalTimeout: TimeInterval {
|
|
49
|
+
totalTimeout ?? timeout
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct Response {
|
|
54
|
+
let data: Any
|
|
55
|
+
let status: Int
|
|
56
|
+
let statusText: String
|
|
57
|
+
let headers: [String: String] // Single-value headers (backward compatible)
|
|
58
|
+
let multiValueHeaders: [String: [String]] // Multi-value headers (e.g., Set-Cookie)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum SniConnectError: Error {
|
|
62
|
+
case invalidURL
|
|
63
|
+
case invalidConfig(String)
|
|
64
|
+
case dnsResolutionFailed(String)
|
|
65
|
+
case tlsHandshakeFailed(String)
|
|
66
|
+
case certificateValidationFailed(String)
|
|
67
|
+
case connectionTimeout
|
|
68
|
+
case connectionRefused
|
|
69
|
+
case networkUnreachable
|
|
70
|
+
case requestTimeout
|
|
71
|
+
case httpError(code: Int, message: String)
|
|
72
|
+
case cancelled
|
|
73
|
+
case unknown(Error)
|
|
74
|
+
|
|
75
|
+
var code: String {
|
|
76
|
+
switch self {
|
|
77
|
+
case .invalidURL: return "SNI_INVALID_URL"
|
|
78
|
+
case .invalidConfig: return "SNI_INVALID_CONFIG"
|
|
79
|
+
case .dnsResolutionFailed: return "SNI_DNS_FAILED"
|
|
80
|
+
case .tlsHandshakeFailed: return "SNI_TLS_FAILED"
|
|
81
|
+
case .certificateValidationFailed: return "SNI_CERT_FAILED"
|
|
82
|
+
case .connectionTimeout: return "SNI_TIMEOUT"
|
|
83
|
+
case .connectionRefused: return "SNI_CONNECTION_REFUSED"
|
|
84
|
+
case .networkUnreachable: return "SNI_NETWORK_UNREACHABLE"
|
|
85
|
+
case .requestTimeout: return "SNI_REQUEST_TIMEOUT"
|
|
86
|
+
case .httpError: return "SNI_HTTP_ERROR"
|
|
87
|
+
case .cancelled: return "SNI_CANCELLED"
|
|
88
|
+
case .unknown: return "SNI_UNKNOWN_ERROR"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var message: String {
|
|
93
|
+
switch self {
|
|
94
|
+
case .invalidURL:
|
|
95
|
+
return "Invalid URL format"
|
|
96
|
+
case .invalidConfig(let details):
|
|
97
|
+
return "Invalid configuration: \(details)"
|
|
98
|
+
case .dnsResolutionFailed(let domain):
|
|
99
|
+
return "DNS resolution failed for domain: \(domain)"
|
|
100
|
+
case .tlsHandshakeFailed(let details):
|
|
101
|
+
return "TLS handshake failed: \(details)"
|
|
102
|
+
case .certificateValidationFailed(let details):
|
|
103
|
+
return "Certificate validation failed: \(details)"
|
|
104
|
+
case .connectionTimeout:
|
|
105
|
+
return "Connection timeout"
|
|
106
|
+
case .connectionRefused:
|
|
107
|
+
return "Connection refused by server"
|
|
108
|
+
case .networkUnreachable:
|
|
109
|
+
return "Network unreachable"
|
|
110
|
+
case .requestTimeout:
|
|
111
|
+
return "Request timeout"
|
|
112
|
+
case .httpError(let code, let message):
|
|
113
|
+
return "HTTP error \(code): \(message)"
|
|
114
|
+
case .cancelled:
|
|
115
|
+
return "Request cancelled"
|
|
116
|
+
case .unknown(let error):
|
|
117
|
+
return "Unknown error: \(error.localizedDescription)"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Convert NSError to SniConnectError with detailed classification
|
|
122
|
+
static func from(_ error: Error) -> SniConnectError {
|
|
123
|
+
let nsError = error as NSError
|
|
124
|
+
|
|
125
|
+
// Check for URL-related errors
|
|
126
|
+
if nsError.domain == NSURLErrorDomain {
|
|
127
|
+
switch nsError.code {
|
|
128
|
+
case NSURLErrorTimedOut:
|
|
129
|
+
return .requestTimeout
|
|
130
|
+
case NSURLErrorCannotConnectToHost:
|
|
131
|
+
return .connectionRefused
|
|
132
|
+
case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
|
|
133
|
+
return .networkUnreachable
|
|
134
|
+
case NSURLErrorSecureConnectionFailed:
|
|
135
|
+
return .tlsHandshakeFailed(nsError.localizedDescription)
|
|
136
|
+
case NSURLErrorServerCertificateUntrusted, NSURLErrorServerCertificateHasBadDate,
|
|
137
|
+
NSURLErrorServerCertificateHasUnknownRoot, NSURLErrorServerCertificateNotYetValid:
|
|
138
|
+
return .certificateValidationFailed(nsError.localizedDescription)
|
|
139
|
+
case NSURLErrorCannotFindHost, NSURLErrorDNSLookupFailed:
|
|
140
|
+
return .dnsResolutionFailed(nsError.localizedDescription)
|
|
141
|
+
case NSURLErrorCancelled:
|
|
142
|
+
return .cancelled
|
|
143
|
+
default:
|
|
144
|
+
return .unknown(error)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return .unknown(error)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static let urlSession: URLSession = {
|
|
153
|
+
let configuration = URLSessionConfiguration.default
|
|
154
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
155
|
+
configuration.urlCache = nil
|
|
156
|
+
configuration.httpCookieStorage = nil
|
|
157
|
+
configuration.httpShouldSetCookies = false
|
|
158
|
+
configuration.shouldUseExtendedBackgroundIdleMode = false
|
|
159
|
+
|
|
160
|
+
let curlConfig = EMASCurlConfiguration.default()
|
|
161
|
+
curlConfig.httpVersion = .HTTP2
|
|
162
|
+
curlConfig.connectTimeoutInterval = 2.5
|
|
163
|
+
curlConfig.enableBuiltInGzip = true
|
|
164
|
+
curlConfig.enableBuiltInRedirection = true
|
|
165
|
+
curlConfig.cacheEnabled = false
|
|
166
|
+
|
|
167
|
+
// Enable full certificate validation for security
|
|
168
|
+
// The certificate will be validated against the SNI hostname, not the IP
|
|
169
|
+
curlConfig.certificateValidationEnabled = true
|
|
170
|
+
curlConfig.domainNameVerificationEnabled = true
|
|
171
|
+
curlConfig.dnsResolver = DNSResolver.self
|
|
172
|
+
|
|
173
|
+
EMASCurlProtocol.install(into: configuration, with: curlConfig)
|
|
174
|
+
return URLSession(configuration: configuration)
|
|
175
|
+
}()
|
|
176
|
+
|
|
177
|
+
@objc private final class DNSResolver: NSObject, EMASCurlProtocolDNSResolver {
|
|
178
|
+
private static let queue = DispatchQueue(label: "com.onekey.sni.connect.dns", attributes: .concurrent)
|
|
179
|
+
private static let cache = DNSCache()
|
|
180
|
+
|
|
181
|
+
/// LRU cache for DNS mappings with size limit and TTL support
|
|
182
|
+
private class DNSCache {
|
|
183
|
+
private struct CacheEntry {
|
|
184
|
+
let ip: String
|
|
185
|
+
let timestamp: TimeInterval
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private var storage: [String: CacheEntry] = [:]
|
|
189
|
+
private var accessOrder: [String] = []
|
|
190
|
+
|
|
191
|
+
// Maximum cache entries (100 as specified in requirements)
|
|
192
|
+
private let maxSize = 100
|
|
193
|
+
// TTL in seconds (5 minutes default)
|
|
194
|
+
private let ttl: TimeInterval = 300
|
|
195
|
+
|
|
196
|
+
func get(_ domain: String) -> String? {
|
|
197
|
+
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)
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
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
|
+
return entry.ip
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
func set(_ ip: String, for domain: String) {
|
|
217
|
+
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()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
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
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func clear() {
|
|
247
|
+
storage.removeAll()
|
|
248
|
+
accessOrder.removeAll()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func cleanExpired() {
|
|
252
|
+
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)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@objc static func resolveDomain(_ domain: String) -> String? {
|
|
261
|
+
var result: String?
|
|
262
|
+
queue.sync {
|
|
263
|
+
result = cache.get(domain)
|
|
264
|
+
}
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static func setIP(_ ip: String, for host: String) {
|
|
269
|
+
queue.sync(flags: .barrier) {
|
|
270
|
+
cache.set(ip, for: host)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Clear all DNS cache entries
|
|
275
|
+
static func clearCache() {
|
|
276
|
+
queue.sync(flags: .barrier) {
|
|
277
|
+
cache.clear()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Clean expired DNS cache entries
|
|
282
|
+
static func cleanExpiredEntries() {
|
|
283
|
+
queue.sync(flags: .barrier) {
|
|
284
|
+
cache.cleanExpired()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Clear all DNS cache entries
|
|
290
|
+
func clearDNSCache() {
|
|
291
|
+
DNSResolver.clearCache()
|
|
292
|
+
onLog?("info", "DNS cache cleared")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Cancel a request by ID
|
|
296
|
+
func cancelRequest(requestId: String) {
|
|
297
|
+
tasksQueue.async(flags: .barrier) { [weak self] in
|
|
298
|
+
guard let task = self?.activeTasks[requestId] else {
|
|
299
|
+
self?.onLog?("warning", "No active request found with ID: \(requestId)")
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
task.cancel()
|
|
303
|
+
self?.activeTasks.removeValue(forKey: requestId)
|
|
304
|
+
self?.onLog?("info", "Request cancelled: \(requestId)")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Cancel all active requests
|
|
309
|
+
func cancelAllRequests() {
|
|
310
|
+
tasksQueue.async(flags: .barrier) { [weak self] in
|
|
311
|
+
guard let self = self else { return }
|
|
312
|
+
let count = self.activeTasks.count
|
|
313
|
+
for (_, task) in self.activeTasks {
|
|
314
|
+
task.cancel()
|
|
315
|
+
}
|
|
316
|
+
self.activeTasks.removeAll()
|
|
317
|
+
self.onLog?("info", "Cancelled \(count) active requests")
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Register an active task (synchronous for immediate registration)
|
|
322
|
+
func registerTask(_ task: Task<Response, Error>, for requestId: String) {
|
|
323
|
+
tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
324
|
+
self?.activeTasks[requestId] = task
|
|
325
|
+
self?.onLog?("info", "Registered request: \(requestId), total active: \(self?.activeTasks.count ?? 0)")
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Unregister a completed or failed task
|
|
330
|
+
private func unregisterTask(requestId: String?) {
|
|
331
|
+
guard let requestId = requestId else { return }
|
|
332
|
+
tasksQueue.async(flags: .barrier) { [weak self] in
|
|
333
|
+
self?.activeTasks.removeValue(forKey: requestId)
|
|
334
|
+
self?.onLog?("info", "Unregistered request: \(requestId)")
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func performRequest(config: RequestConfig) async throws -> Response {
|
|
339
|
+
// Check if task is cancelled
|
|
340
|
+
try Task.checkCancellation()
|
|
341
|
+
|
|
342
|
+
defer {
|
|
343
|
+
unregisterTask(requestId: config.requestId)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
DNSResolver.setIP(config.ip, for: config.hostname)
|
|
347
|
+
|
|
348
|
+
let url = try Self.buildURL(hostname: config.hostname, path: config.path)
|
|
349
|
+
|
|
350
|
+
let mutableRequest = NSMutableURLRequest(url: url)
|
|
351
|
+
mutableRequest.httpMethod = config.method.uppercased()
|
|
352
|
+
|
|
353
|
+
// Convert milliseconds to seconds for timeout values
|
|
354
|
+
let totalTimeoutSeconds = config.effectiveTotalTimeout / 1000.0
|
|
355
|
+
let connectTimeoutSeconds = config.effectiveConnectTimeout / 1000.0
|
|
356
|
+
|
|
357
|
+
// Set total request timeout
|
|
358
|
+
mutableRequest.timeoutInterval = totalTimeoutSeconds
|
|
359
|
+
mutableRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
|
360
|
+
|
|
361
|
+
// Explicitly set Host header for SNI
|
|
362
|
+
mutableRequest.setValue(config.hostname, forHTTPHeaderField: "Host")
|
|
363
|
+
|
|
364
|
+
for (key, value) in config.headers {
|
|
365
|
+
if key.caseInsensitiveCompare("host") == .orderedSame {
|
|
366
|
+
// Host header is already set above, skip duplicate
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
mutableRequest.setValue(value, forHTTPHeaderField: key)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if let bodyString = config.body, let bodyData = bodyString.data(using: .utf8) {
|
|
373
|
+
mutableRequest.httpBody = bodyData
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Configure connection timeout for EMASCurl
|
|
377
|
+
EMASCurlProtocol.setConnectTimeoutInterval(connectTimeoutSeconds)
|
|
378
|
+
let request = mutableRequest as URLRequest
|
|
379
|
+
|
|
380
|
+
do {
|
|
381
|
+
let (data, response) = try await Self.urlSession.data(for: request)
|
|
382
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
383
|
+
let errorMsg = "Invalid HTTP response type"
|
|
384
|
+
onLog?("error", errorMsg)
|
|
385
|
+
throw SniConnectError.invalidConfig(errorMsg)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let status = httpResponse.statusCode
|
|
389
|
+
let parsedData = Self.parseResponseData(data)
|
|
390
|
+
let (headers, multiValueHeaders) = Self.extractHeaders(from: httpResponse)
|
|
391
|
+
let statusText = HTTPURLResponse.localizedString(forStatusCode: status)
|
|
392
|
+
|
|
393
|
+
// Check for HTTP errors (4xx, 5xx)
|
|
394
|
+
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
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return Response(
|
|
401
|
+
data: parsedData,
|
|
402
|
+
status: status,
|
|
403
|
+
statusText: statusText,
|
|
404
|
+
headers: headers,
|
|
405
|
+
multiValueHeaders: multiValueHeaders
|
|
406
|
+
)
|
|
407
|
+
} catch let error as SniConnectError {
|
|
408
|
+
onLog?("error", "[\(error.code)] \(error.message)")
|
|
409
|
+
throw error
|
|
410
|
+
} catch {
|
|
411
|
+
// Convert generic errors to specific SniConnectError types
|
|
412
|
+
let sniError = SniConnectError.from(error)
|
|
413
|
+
onLog?("error", "[\(sniError.code)] \(sniError.message)")
|
|
414
|
+
throw sniError
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
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
|
|
430
|
+
} else {
|
|
431
|
+
normalizedPath = "/" + trimmedPath
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
guard let url = URL(string: "https://\(hostname)\(normalizedPath)") else {
|
|
435
|
+
throw SniConnectError.invalidURL
|
|
436
|
+
}
|
|
437
|
+
return url
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private static func parseResponseData(_ data: Data) -> Any {
|
|
441
|
+
guard !data.isEmpty else {
|
|
442
|
+
return [:]
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
|
|
446
|
+
return jsonObject
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if let text = String(data: data, encoding: .utf8) {
|
|
450
|
+
return text
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return data.base64EncodedString()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/// Extract headers from HTTP response
|
|
457
|
+
/// Returns both single-value headers (for backward compatibility) and multi-value headers
|
|
458
|
+
private static func extractHeaders(from response: HTTPURLResponse) -> ([String: String], [String: [String]]) {
|
|
459
|
+
var singleValueHeaders: [String: String] = [:]
|
|
460
|
+
var multiValueHeaders: [String: [String]] = [:]
|
|
461
|
+
|
|
462
|
+
// Group headers by normalized key (lowercase)
|
|
463
|
+
var headerGroups: [String: [String]] = [:]
|
|
464
|
+
|
|
465
|
+
for (key, value) in response.allHeaderFields {
|
|
466
|
+
let headerKey = String(describing: key).lowercased()
|
|
467
|
+
let headerValue = String(describing: value)
|
|
468
|
+
|
|
469
|
+
if headerGroups[headerKey] == nil {
|
|
470
|
+
headerGroups[headerKey] = []
|
|
471
|
+
}
|
|
472
|
+
headerGroups[headerKey]?.append(headerValue)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Process grouped headers
|
|
476
|
+
for (key, values) in headerGroups {
|
|
477
|
+
// For backward compatibility, single-value headers use the last value
|
|
478
|
+
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
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return (singleValueHeaders, multiValueHeaders)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["@types/react-native-codegen.d.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeModules, Platform, TurboModuleRegistry } from 'react-native';
|
|
4
|
+
const LINKING_ERROR = `The package '@onekeyfe/react-native-sni-connect' doesn't seem to be linked. Make sure:\n\n` + Platform.select({
|
|
5
|
+
ios: "- You have run 'pod install'\n",
|
|
6
|
+
default: ''
|
|
7
|
+
}) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go; create a custom dev client instead\n';
|
|
8
|
+
const turboModuleResult = TurboModuleRegistry.get('SniConnect');
|
|
9
|
+
const turboModule = turboModuleResult ?? null;
|
|
10
|
+
const bridgeModule = NativeModules.SniConnect ?? null;
|
|
11
|
+
const nativeModule = turboModule ?? bridgeModule;
|
|
12
|
+
if (nativeModule == null) {
|
|
13
|
+
throw new Error(LINKING_ERROR);
|
|
14
|
+
}
|
|
15
|
+
const NativeSniConnect = nativeModule;
|
|
16
|
+
export default NativeSniConnect;
|
|
17
|
+
//# sourceMappingURL=NativeSniConnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeModules","Platform","TurboModuleRegistry","LINKING_ERROR","select","ios","default","turboModuleResult","get","turboModule","bridgeModule","SniConnect","nativeModule","Error","NativeSniConnect"],"sourceRoot":"../../src","sources":["NativeSniConnect.ts"],"mappings":";;AAAA,SACEA,aAAa,EACbC,QAAQ,EACRC,mBAAmB,QAEd,cAAc;AAoCrB,MAAMC,aAAa,GACjB,4FAA4F,GAC5FF,QAAQ,CAACG,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,mEAAmE;AASrE,MAAMC,iBAAiB,GAAGL,mBAAmB,CAACM,GAAG,CAAO,YAAY,CAAC;AAErE,MAAMC,WAAwB,GAAGF,iBAAiB,IAAI,IAAI;AAE1D,MAAMG,YAAyB,GAC5BV,aAAa,CAACW,UAAU,IAAyB,IAAI;AAExD,MAAMC,YAAY,GAAIH,WAAW,IAAIC,YAAwC;AAE7E,IAAIE,YAAY,IAAI,IAAI,EAAE;EACxB,MAAM,IAAIC,KAAK,CAACV,aAAa,CAAC;AAChC;AAEA,MAAMW,gBAAkC,GAAGF,YAAY;AAEvD,eAAeE,gBAAgB","ignoreList":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeEventEmitter } from 'react-native';
|
|
4
|
+
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
|
+
export function request(config) {
|
|
20
|
+
return NativeSniConnect.request(config);
|
|
21
|
+
}
|
|
22
|
+
export function cancelRequest(requestId) {
|
|
23
|
+
return NativeSniConnect.cancelRequest(requestId);
|
|
24
|
+
}
|
|
25
|
+
export function cancelAllRequests() {
|
|
26
|
+
return NativeSniConnect.cancelAllRequests();
|
|
27
|
+
}
|
|
28
|
+
export function clearDNSCache() {
|
|
29
|
+
return NativeSniConnect.clearDNSCache();
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeEventEmitter","NativeSniConnect","eventEmitter","subscribeToLogs","callback","subscription","addListener","log","remove","request","config","cancelRequest","requestId","cancelAllRequests","clearDNSCache"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,cAAc;AACjD,OAAOC,gBAAgB,MAGhB,uBAAoB;;AAE3B;;AAOA;AACA,MAAMC,YAAY,GAAG,IAAIF,kBAAkB,CAACC,gBAAuB,CAAC;;AAEpE;AACA,OAAO,SAASE,eAAeA,CAACC,QAAiC,EAAc;EAC7E,MAAMC,YAAY,GAAGH,YAAY,CAACI,WAAW,CAAC,eAAe,EAAGC,GAAQ,IAAK;IAC3E;IACAH,QAAQ,CAACG,GAAe,CAAC;EAC3B,CAAC,CAAC;EACF,OAAO,MAAMF,YAAY,CAACG,MAAM,CAAC,CAAC;AACpC;AAEA,OAAO,SAASC,OAAOA,CACrBC,MAAyB,EACI;EAC7B,OAAOT,gBAAgB,CAACQ,OAAO,CAACC,MAAM,CAAC;AACzC;AAEA,OAAO,SAASC,aAAaA,CAC3BC,SAAiB,EACc;EAC/B,OAAOX,gBAAgB,CAACU,aAAa,CAACC,SAAS,CAAC;AAClD;AAEA,OAAO,SAASC,iBAAiBA,CAAA,EAAkC;EACjE,OAAOZ,gBAAgB,CAACY,iBAAiB,CAAC,CAAC;AAC7C;AAEA,OAAO,SAASC,aAAaA,CAAA,EAAkC;EAC7D,OAAOb,gBAAgB,CAACa,aAAa,CAAC,CAAC;AACzC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type TurboModule } from 'react-native';
|
|
2
|
+
import type { Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
type HeaderMap = {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
};
|
|
6
|
+
export type SniConnectRequest = {
|
|
7
|
+
requestId?: string;
|
|
8
|
+
ip: string;
|
|
9
|
+
hostname: string;
|
|
10
|
+
method: string;
|
|
11
|
+
path: string;
|
|
12
|
+
headers: HeaderMap;
|
|
13
|
+
body?: string | null;
|
|
14
|
+
timeout: Double;
|
|
15
|
+
};
|
|
16
|
+
export type SniConnectResponse = {
|
|
17
|
+
data: string;
|
|
18
|
+
status: Int32;
|
|
19
|
+
statusText: string;
|
|
20
|
+
headers: HeaderMap;
|
|
21
|
+
};
|
|
22
|
+
export interface Spec extends TurboModule {
|
|
23
|
+
request(config: SniConnectRequest): Promise<SniConnectResponse>;
|
|
24
|
+
cancelRequest(requestId: string): Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
cancelAllRequests(): Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
clearDNSCache(): Promise<{
|
|
31
|
+
success: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
addListener(eventType: string): void;
|
|
34
|
+
removeListeners(count: Int32): void;
|
|
35
|
+
}
|
|
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
|
+
};
|
|
48
|
+
declare const NativeSniConnect: SniConnectModule;
|
|
49
|
+
export default NativeSniConnect;
|
|
50
|
+
//# sourceMappingURL=NativeSniConnect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeSniConnect.d.ts","sourceRoot":"","sources":["../../../src/NativeSniConnect.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,2CAA2C,CAAC;AAE/E,KAAK,SAAS,GAAG;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,SAAS,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChE,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnD,aAAa,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAG/C,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrC;AAQD,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG;IACpC,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChE,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnD,aAAa,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAChD,CAAC;AAeF,QAAA,MAAM,gBAAgB,EAAE,gBAA+B,CAAC;AAExD,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export declare function request(config: SniConnectRequest): Promise<SniConnectResponse>;
|
|
9
|
+
export declare function cancelRequest(requestId: string): Promise<{
|
|
10
|
+
success: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function cancelAllRequests(): Promise<{
|
|
13
|
+
success: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function clearDNSCache(): Promise<{
|
|
16
|
+
success: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
export type { SniConnectRequest, SniConnectResponse } from './NativeSniConnect';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAyB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI,CAM7E;AAED,wBAAgB,OAAO,CACrB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAE/B;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAEjE;AAED,wBAAgB,aAAa,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAE7D;AAED,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
|