QuickDataTask.swift (11050B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 * @author codex gpt5.6-sol 8 */ 9 import Foundation 10 //import "Foundation/NSURLError.h" 11 import os.log 12 13 import FTalerWalletcore 14 15 // will be called from wallet-core for networking 16 func request_create(userdata: Optional<UnsafeMutableRawPointer>, 17 requestInfo: Optional<UnsafeMutablePointer<JSHttpRequestInfo>>) -> Int32 { 18 let useHTTP3 = false // <<== make this a parameter 19 let quickjs = Unmanaged<Quickjs>.fromOpaque(userdata!).takeUnretainedValue() 20 21 if let requestInfo { 22 if let url = URL(string: String(cString: requestInfo.pointee.url)) { 23 let responseCb = requestInfo.pointee.response_cb 24 let responseCbCls = requestInfo.pointee.response_cb_cls 25 let method = String(cString: requestInfo.pointee.method) 26 let requestHeaders = requestInfo.pointee.request_headers 27 let redirect = requestInfo.pointee.redirect // TODO: redirect 28 let timeoutMs = requestInfo.pointee.timeout_ms 29 let debug = requestInfo.pointee.debug // TODO: debug 30 let reqBody = requestInfo.pointee.req_body 31 let bodyLen = requestInfo.pointee.req_body_len 32 33 var request = URLRequest(url: url) 34 request.httpMethod = method 35 if useHTTP3 { 36 // if server endpoint is known to support HTTP/3. Enables QUIC 37 // racing without HTTP/3 service discovery. Defaults to `false`. 38 request.assumesHTTP3Capable = true 39 } 40 switch timeoutMs { 41 case 0: // leave URLSession's default 42 break 43 case ..<0: // "no timeout" 44 request.timeoutInterval = 60 * 60 * 24 45 default: 46 request.timeoutInterval = TimeInterval(timeoutMs) / 1000.0 47 } 48 if let reqBody, bodyLen > 0 { // qtart frees req_body as soon as request_create returns, 49 request.httpBody = Data(bytes: reqBody, count: Int(bodyLen)) // thus we must copy it 50 } 51 if var ptr = requestHeaders { 52 while let cString = ptr.pointee { 53 let string = String(cString: cString) 54 if let index = string.firstIndex(of: ":") { 55 let headerField = string.prefix(upTo: index) 56 let nextIndex = string.index(index, offsetBy: 1) // skip the ":" 57 let value = string.suffix(from: nextIndex) 58 request.addValue(String(value), forHTTPHeaderField: String(headerField)) 59 } 60 ptr += 1 61 } 62 } 63 return quickjs.reqCreate(request, responseCb, responseCbCls) 64 } 65 } 66 let logger = Logger(subsystem: "net.taler.gnu", category: "Networking Failure") 67 logger.error("Failed to create URLRequest for >>\(String(describing: requestInfo?.pointee.url))<<") 68 return -1 69 } 70 71 func request_cancel(userdata: Optional<UnsafeMutableRawPointer>, 72 requestID: Int32) -> Int32 { 73 let quickjs = Unmanaged<Quickjs>.fromOpaque(userdata!).takeUnretainedValue() 74 return quickjs.reqCancel(requestID) 75 } 76 // MARK: - 77 extension Error { 78 var errorCode:Int? { 79 return (self as NSError).code 80 } 81 } 82 // MARK: - 83 final class QuickDataTask: NSObject, @unchecked Sendable { 84 let urlSession: URLSession 85 let request: URLRequest 86 let requestID: Int32 87 let responseCb: JSHttpResponseCb? 88 let responseCbCls: Optional<UnsafeMutableRawPointer> 89 90 private let state = NSCondition() 91 private let onFinish: (Int32) -> Void 92 private var dataTask: URLSessionDataTask? 93 private var isCancelled = false 94 private var isCallbackActive = false 95 private var isFinished = false 96 #if DEBUG || TALER_NIGHTLY 97 private let logger: Logger 98 #endif 99 100 init(urlSession: URLSession, 101 request: URLRequest, 102 requestID: Int32, 103 responseCb: JSHttpResponseCb?, 104 responseCbCls: Optional<UnsafeMutableRawPointer>, 105 onFinish: @escaping (Int32) -> Void 106 ) { 107 #if DEBUG || TALER_NIGHTLY 108 self.logger = Logger(subsystem: "net.taler.gnu", category: "Networking") 109 #endif 110 self.urlSession = urlSession 111 self.request = request 112 self.requestID = requestID 113 self.responseCb = responseCb 114 self.responseCbCls = responseCbCls 115 self.onFinish = onFinish 116 } 117 118 func run() { 119 guard responseCb != nil, responseCbCls != nil else { 120 finish() 121 return 122 } 123 124 let method = request.httpMethod ?? "Unknown" 125 let url = request.url?.trimmedString ?? EMPTYSTRING 126 #if DEBUG || TALER_NIGHTLY 127 logger.trace("❓\(self.requestID, privacy: .public) \(method, privacy: .public) \(url, privacy: .public)") 128 #endif 129 let task = urlSession.dataTask(with: request) { [self] data, response, error in 130 handleCompletion(data: data, 131 response: response, 132 error: error, 133 method: method, 134 url: url) 135 } 136 137 state.lock() 138 guard !isFinished else { 139 state.unlock() 140 task.cancel() 141 return 142 } 143 dataTask = task 144 let shouldStart = !isCancelled 145 state.unlock() 146 147 if shouldStart { 148 task.resume() 149 } else { 150 task.cancel() 151 finish() 152 } 153 } 154 155 /// Cancels the request and does not return until a native callback that had 156 /// already started has completed. A URLSession completion may still arrive 157 /// later, but `isCancelled` prevents it from entering the native callback. 158 func cancel() { 159 state.lock() 160 guard !isFinished else { 161 state.unlock() 162 return 163 } 164 isCancelled = true 165 let task = dataTask 166 state.unlock() 167 168 task?.cancel() 169 170 var shouldNotify = false 171 state.lock() 172 while isCallbackActive { 173 state.wait() 174 } 175 if !isFinished { 176 isFinished = true 177 dataTask = nil 178 shouldNotify = true 179 } 180 state.unlock() 181 182 if shouldNotify { 183 onFinish(requestID) 184 } 185 } 186 187 private func handleCompletion(data: Data?, 188 response: URLResponse?, 189 error: Error?, 190 method: String, 191 url: String) { 192 guard beginCallback() else { 193 finish() 194 return 195 } 196 defer { endCallback() } 197 198 if let response = response as? HTTPURLResponse { 199 deliver(response: response, data: data ?? Data(), method: method, url: url) 200 } else { 201 #if DEBUG || TALER_NIGHTLY 202 logger.error("⁉️ \(self.requestID, privacy: .public) \(method, privacy: .public) \(error, privacy: .public)") 203 #endif 204 Task.detached { 205 Controller.shared.checkInternetConnection() 206 } 207 deliver(error: error) 208 } 209 } 210 211 private func deliver(response: HTTPURLResponse, 212 data: Data, 213 method: String, 214 url: String) { 215 guard let responseCb, let responseCbCls else { 216 return 217 } 218 219 let status = Int32(response.statusCode) 220 let errmsg = HTTPURLResponse.localizedString(forStatusCode: response.statusCode) 221 let headerArray = response.allHeaderFields.map { "\($0.key): \($0.value)" } 222 let cHeaders = CStringArray(headerArray) 223 224 #if DEBUG || TALER_NIGHTLY 225 if data.isEmpty { 226 logger.error("‼️\(self.requestID, privacy: .public) \(method, privacy: .public) \(response.statusCode, privacy: .public) \(errmsg, privacy: .public)") 227 } else { 228 logger.trace("❗️ \(self.requestID, privacy: .public) \(url, privacy: .public)") 229 } 230 #endif 231 232 errmsg.withCString { errmsgPtr in 233 data.withUnsafeBytes { bodyBytes in 234 var responseInfo = JSHttpResponseInfo( 235 request_id: requestID, 236 status: status, 237 errmsg: UnsafeMutablePointer(mutating: errmsgPtr), 238 response_headers: cHeaders.pointer, 239 num_response_headers: Int32(headerArray.count), 240 body: UnsafeMutableRawPointer(mutating: bodyBytes.baseAddress), 241 body_len: numericCast(data.count) 242 ) 243 withUnsafeMutablePointer(to: &responseInfo) { responseInfoPtr in 244 // cHeaders owns the C strings that response_headers points to, and ARC 245 // only keeps it alive up to its last use - which is reading .pointer 246 // above, before the callback that dereferences it has run 247 withExtendedLifetime(cHeaders) { 248 responseCb(responseCbCls, responseInfoPtr) 249 } 250 } 251 } 252 } 253 } 254 255 private func deliver(error: Error?) { 256 guard let responseCb, let responseCbCls else { 257 return 258 } 259 260 let errmsg = error?.localizedDescription ?? "Network request failed" 261 errmsg.withCString { errmsgPtr in 262 var responseInfo = JSHttpResponseInfo( 263 request_id: requestID, 264 status: 0, 265 errmsg: UnsafeMutablePointer(mutating: errmsgPtr), 266 response_headers: nil, 267 num_response_headers: 0, 268 body: nil, 269 body_len: 0 270 ) 271 withUnsafeMutablePointer(to: &responseInfo) { responseInfoPtr in 272 responseCb(responseCbCls, responseInfoPtr) 273 } 274 } 275 } 276 277 private func beginCallback() -> Bool { 278 state.lock() 279 defer { state.unlock() } 280 guard !isCancelled, !isFinished else { 281 return false 282 } 283 isCallbackActive = true 284 return true 285 } 286 287 private func endCallback() { 288 var shouldNotify = false 289 state.lock() 290 isCallbackActive = false 291 if !isFinished { 292 isFinished = true 293 dataTask = nil 294 shouldNotify = true 295 } 296 state.broadcast() 297 state.unlock() 298 299 if shouldNotify { 300 onFinish(requestID) 301 } 302 } 303 304 private func finish() { 305 var shouldNotify = false 306 state.lock() 307 if !isFinished { 308 isFinished = true 309 dataTask = nil 310 shouldNotify = true 311 } 312 state.broadcast() 313 state.unlock() 314 315 if shouldNotify { 316 onFinish(requestID) 317 } 318 } 319 }