REF: SwiftTCPClient to use Network framework and Backgroud thread

This commit is contained in:
Marcos Rodriguez Velez 2024-11-17 18:55:37 -04:00
parent 4307248e33
commit e72ba878e2
3 changed files with 196 additions and 192 deletions

View File

@ -15,55 +15,56 @@ struct APIError: LocalizedError {
extension MarketAPI {
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
let settings = userElectrumSettings
let portToUse = settings.sslPort ?? settings.port
let isSSLSupported = settings.sslPort != nil
DispatchQueue.global(qos: .background).async {
Task {
let client = SwiftTCPClient()
defer {
print("Closing connection to \(String(describing: settings.host)):\(String(describing: portToUse)).")
print("Closing connection to \(userElectrumSettings.host ?? "unknown"):\(userElectrumSettings.sslPort ?? userElectrumSettings.port ?? 0).")
client.close()
}
guard let host = settings.host, let portToUse = portToUse else { return }
guard let host = userElectrumSettings.host, let portToUse = userElectrumSettings.sslPort ?? userElectrumSettings.port else {
completion(nil, APIError())
return
}
print("Attempting to connect to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
let isSSLSupported = userElectrumSettings.sslPort != nil
print("Attempting to connect to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
if client.connect(to: host, port: UInt32(portToUse), useSSL: isSSLSupported) {
print("Successfully connected to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
let connected = await client.connect(to: host, port: portToUse, useSSL: isSSLSupported)
if connected {
print("Successfully connected to \(host):\(portToUse) with SSL: \(isSSLSupported).")
} else {
print("Failed to connect to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
print("Failed to connect to \(host):\(portToUse) with SSL: \(isSSLSupported).")
completion(nil, APIError())
return
}
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
guard let data = message.data(using: .utf8), client.send(data: data) else {
print("Message sending failed to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
guard let data = message.data(using: .utf8), await client.send(data: data) else {
print("Message sending failed to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
completion(nil, APIError())
return
}
print("Message sent successfully to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
print("Message sent successfully to \(host):\(portToUse) with SSL: \(isSSLSupported).")
do {
let receivedData = try client.receive()
let receivedData = try await client.receive()
print("Data received. Parsing...")
guard let responseString = String(data: receivedData, encoding: .utf8),
let responseData = responseString.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments) as? [String: AnyObject],
let feeHistogram = json["result"] as? [[Double]] else {
print("Failed to parse response from \(String(describing: settings.host)).")
print("Failed to parse response from \(host).")
completion(nil, APIError())
return
}
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0)
completion(marketData, nil) // Successfully fetched data, return it
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0, dateString: "")
print("Parsed MarketData: \(marketData)")
completion(marketData, nil)
} catch {
print("Error receiving data from \(String(describing: settings.host)): \(error.localizedDescription)")
print("Error receiving data from \(host): \(error.localizedDescription)")
completion(nil, APIError())
}
}

View File

@ -9,9 +9,9 @@
import Foundation
struct UserDefaultsElectrumSettings {
let host: String?
let port: Int32?
let sslPort: Int32?
var host: String?
var port: UInt16?
var sslPort: UInt16?
}
let hardcodedPeers = [
@ -34,14 +34,14 @@ class UserDefaultsGroup {
return DefaultElectrumPeers.randomElement()!
}
let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? "50001"
let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? "443"
let electrumSettingsTCPPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? 50001
let electrumSettingsSSLPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? 443
let host = electrumSettingsHost
let sslPort = Int32(electrumSettingsSSLPort)
let port = Int32(electrumSettingsTCPPort)
let sslPort = electrumSettingsSSLPort
let port = electrumSettingsTCPPort
return UserDefaultsElectrumSettings(host: host, port: port, sslPort: sslPort)
return UserDefaultsElectrumSettings(host: host, port: port as! UInt16, sslPort: sslPort as! UInt16)
}
static func getAllWalletsBalance() -> Double {

View File

@ -1,152 +1,155 @@
// BlueWallet
//
// Created by Marcos Rodriguez on 3/23/23.
// Copyright © 2023 BlueWallet. All rights reserved.
import Foundation
import Network
/**
`SwiftTCPClient` is a simple TCP client class that allows for establishing a TCP connection,
sending data, and receiving data over the network. It supports both plain TCP and SSL-secured connections.
enum SwiftTCPClientError: Error, LocalizedError {
case connectionNil
case connectionCancelled
case readTimedOut
case noDataReceived
case unknown(Error)
The class uses `InputStream` and `OutputStream` for network communication, encapsulating the complexity of stream management and data transfer.
- Note: When using SSL, this implementation disables certificate chain validation for simplicity. This is not recommended for production code due to security risks.
## Examples
### Creating an instance and connecting to a server:
```swift
let client = SwiftTCPClient()
let success = client.connect(to: "example.com", port: 12345, useSSL: false)
if success {
print("Connected successfully.")
} else {
print("Failed to connect.")
var errorDescription: String? {
switch self {
case .connectionNil:
return "Connection is nil."
case .connectionCancelled:
return "Connection was cancelled."
case .readTimedOut:
return "Read timed out."
case .noDataReceived:
return "No data received."
case .unknown(let error):
return error.localizedDescription
}
**/
class SwiftTCPClient: NSObject {
private var inputStream: InputStream?
private var outputStream: OutputStream?
private let bufferSize = 1024
private var readData = Data()
private let readTimeout = 5.0 // Timeout in seconds
func connect(to host: String, port: UInt32, useSSL: Bool = false) -> Bool {
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host as CFString, port, &readStream, &writeStream)
guard let read = readStream?.takeRetainedValue(), let write = writeStream?.takeRetainedValue() else {
return false
}
}
inputStream = read as InputStream
outputStream = write as OutputStream
class SwiftTCPClient {
private var connection: NWConnection?
private let queue = DispatchQueue(label: "SwiftTCPClientQueue")
private let readTimeout: TimeInterval = 5.0
func connect(to host: String, port: UInt16, useSSL: Bool = false) async -> Bool {
let parameters: NWParameters
if useSSL {
// Configure SSL settings for the streams
let sslSettings: [NSString: Any] = [
kCFStreamSSLLevel as NSString: kCFStreamSocketSecurityLevelNegotiatedSSL as Any,
kCFStreamSSLValidatesCertificateChain as NSString: kCFBooleanFalse as Any
// Note: Disabling certificate chain validation (kCFStreamSSLValidatesCertificateChain: kCFBooleanFalse)
// is typically not recommended for production code as it introduces significant security risks.
]
inputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
outputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
parameters = NWParameters(tls: createTLSOptions(), tcp: .init())
} else {
parameters = NWParameters.tcp
}
inputStream?.delegate = self
outputStream?.delegate = self
connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!, using: parameters)
connection?.start(queue: queue)
inputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
outputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
let serialQueue = DispatchQueue(label: "SwiftTCPClient.connect.serialQueue")
var hasResumed = false
inputStream?.open()
outputStream?.open()
return true
}
func send(data: Data) -> Bool {
guard let outputStream = outputStream else {
return false
}
let bytesWritten = data.withUnsafeBytes { bufferPointer -> Int in
guard let baseAddress = bufferPointer.baseAddress else {
return 0
}
return outputStream.write(baseAddress.assumingMemoryBound(to: UInt8.self), maxLength: data.count)
}
return bytesWritten == data.count
}
func receive() throws -> Data {
guard let inputStream = inputStream else {
throw NSError(domain: "SwiftTCPClientError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Input stream is nil."])
}
// Check if the input stream is ready for reading
if inputStream.streamStatus != .open && inputStream.streamStatus != .reading {
throw NSError(domain: "SwiftTCPClientError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Stream is not ready for reading."])
}
readData = Data()
// Wait for data to be available or timeout
let timeoutDate = Date().addingTimeInterval(readTimeout)
repeat {
RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.1))
if readData.count > 0 || Date() > timeoutDate {
break
}
} while inputStream.streamStatus == .open || inputStream.streamStatus == .reading
if readData.count == 0 && Date() > timeoutDate {
throw NSError(domain: "SwiftTCPClientError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Read timed out."])
}
return readData
}
func close() {
inputStream?.close()
outputStream?.close()
inputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
outputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
inputStream = nil
outputStream = nil
}
}
extension SwiftTCPClient: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .hasBytesAvailable:
if aStream == inputStream, let inputStream = inputStream {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
while inputStream.hasBytesAvailable {
let bytesRead = inputStream.read(buffer, maxLength: bufferSize)
if bytesRead > 0 {
readData.append(buffer, count: bytesRead)
}
}
buffer.deallocate()
}
case .errorOccurred:
print("Stream error occurred")
case .endEncountered:
close()
do {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
connection?.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
serialQueue.async {
if !hasResumed {
switch state {
case .ready:
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume()
case .failed(let error):
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume(throwing: error)
case .cancelled:
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume(throwing: SwiftTCPClientError.connectionCancelled)
default:
break
}
}
}
}
}
return true
} catch {
print("Connection failed with error: \(error.localizedDescription)")
return false
}
}
func send(data: Data) async -> Bool {
guard let connection = connection else {
print("Send failed: No active connection.")
return false
}
do {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
connection.send(content: data, completion: .contentProcessed { error in
if let error = error {
print("Send error: \(error.localizedDescription)")
continuation.resume(throwing: error)
} else {
continuation.resume()
}
})
}
return true
} catch {
print("Send failed with error: \(error.localizedDescription)")
return false
}
}
func receive() async throws -> Data {
guard let connection = connection else {
throw SwiftTCPClientError.connectionNil
}
return try await withThrowingTaskGroup(of: Data.self) { group in
group.addTask {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
if let error = error {
continuation.resume(throwing: SwiftTCPClientError.unknown(error))
return
}
if let data = data, !data.isEmpty {
continuation.resume(returning: data)
} else if isComplete {
self.close()
continuation.resume(throwing: SwiftTCPClientError.noDataReceived)
} else {
continuation.resume(throwing: SwiftTCPClientError.readTimedOut)
}
}
}
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(self.readTimeout * 1_000_000_000))
throw SwiftTCPClientError.readTimedOut
}
if let firstResult = try await group.next() {
group.cancelAll()
return firstResult
} else {
throw SwiftTCPClientError.readTimedOut
}
}
}
func close() {
connection?.cancel()
connection = nil
}
private func createTLSOptions() -> NWProtocolTLS.Options {
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completion in
completion(true)
}, DispatchQueue.global(qos: .background))
return tlsOptions
}
}