taler-ios

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

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 }