taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

QuickDataTask.swift (10741B)


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