-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
EasyHandle.swift
686 lines (636 loc) · 29.9 KB
/
EasyHandle.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
// Foundation/URLSession/EasyHandle.swift - URLSession & libcurl
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
// -----------------------------------------------------------------------------
///
/// libcurl *easy handle* wrapper.
/// These are libcurl helpers for the URLSession API code.
/// - SeeAlso: https://curl.haxx.se/libcurl/c/
/// - SeeAlso: URLSession.swift
///
// -----------------------------------------------------------------------------
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import SwiftFoundation
#else
import Foundation
#endif
import CoreFoundation
import CFURLSessionInterface
import Dispatch
/// Minimal wrapper around the [curl easy interface](https://curl.haxx.se/libcurl/c/)
///
/// An *easy handle* manages the state of a transfer inside libcurl.
///
/// As such the easy handle's responsibility is implementing the HTTP
/// protocol while the *multi handle* is in charge of managing sockets and
/// reading from / writing to these sockets.
///
/// An easy handle is added to a multi handle in order to associate it with
/// an actual socket. The multi handle will then feed bytes into the easy
/// handle and read bytes from the easy handle. But this process is opaque
/// to use. It is further worth noting, that with HTTP/1.1 persistent
/// connections and with HTTP/2 there's a 1-to-many relationship between
/// TCP streams and HTTP transfers / easy handles. A single TCP stream and
/// its socket may be shared by multiple easy handles.
///
/// A single HTTP request-response exchange (refered to here as a
/// *transfer*) corresponds directly to an easy handle. Hence anything that
/// needs to be configured for a specific transfer (e.g. the URL) will be
/// configured on an easy handle.
///
/// A single `URLSessionTask` may do multiple, consecutive transfers, and
/// as a result it will have to reconfigure its easy handle between
/// transfers. An easy handle can be re-used once its transfer has
/// completed.
///
/// - Note: All code assumes that it is being called on a single thread /
/// `Dispatch` only -- it is intentionally **not** thread safe.
internal final class _EasyHandle {
let rawHandle = CFURLSessionEasyHandleInit()
weak var delegate: _EasyHandleDelegate?
fileprivate var headerList: _CurlStringList?
fileprivate var pauseState: _PauseState = []
internal var timeoutTimer: _TimeoutSource!
internal lazy var errorBuffer = [UInt8](repeating: 0, count: Int(CFURLSessionEasyErrorSize))
internal var _config: URLSession._Configuration? = nil
internal var _url: URL? = nil
init(delegate: _EasyHandleDelegate) {
self.delegate = delegate
setupCallbacks()
}
deinit {
CFURLSessionEasyHandleDeinit(rawHandle)
}
}
extension _EasyHandle: Equatable {}
internal func ==(lhs: _EasyHandle, rhs: _EasyHandle) -> Bool {
return lhs.rawHandle == rhs.rawHandle
}
extension _EasyHandle {
enum _Action {
case abort
case proceed
case pause
}
enum _WriteBufferResult {
case abort
case pause
/// Write the given number of bytes into the buffer
case bytes(Int)
}
}
internal extension _EasyHandle {
func completedTransfer(withError error: NSError?) {
delegate?.transferCompleted(withError: error)
}
}
internal protocol _EasyHandleDelegate: class {
/// Handle data read from the network.
/// - returns: the action to be taken: abort, proceed, or pause.
func didReceive(data: Data) -> _EasyHandle._Action
/// Handle header data read from the network.
/// - returns: the action to be taken: abort, proceed, or pause.
func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action
/// Fill a buffer with data to be sent.
///
/// - parameter data: The buffer to fill
/// - returns: the number of bytes written to the `data` buffer, or `nil` to stop the current transfer immediately.
func fill(writeBuffer buffer: UnsafeMutableBufferPointer<Int8>) -> _EasyHandle._WriteBufferResult
/// The transfer for this handle completed.
/// - parameter errorCode: An NSURLError code, or `nil` if no error occurred.
func transferCompleted(withError error: NSError?)
/// Seek the input stream to the given position
func seekInputStream(to position: UInt64) throws
/// Gets called during the transfer to update progress.
func updateProgressMeter(with propgress: _EasyHandle._Progress)
}
extension _EasyHandle {
func set(verboseModeOn flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionVERBOSE, flag ? 1 : 0).asError()
}
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CFURLSessionOptionDEBUGFUNCTION.html
func set(debugOutputOn flag: Bool, task: URLSessionTask) {
if flag {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(task).toOpaque())).asError()
try! CFURLSession_easy_setopt_dc(rawHandle, CFURLSessionOptionDEBUGFUNCTION, printLibcurlDebug(handle:type:data:size:userInfo:)).asError()
} else {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGDATA, nil).asError()
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGFUNCTION, nil).asError()
}
}
func set(passHeadersToDataStream flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHEADER, flag ? 1 : 0).asError()
}
/// Follow any Location: header that the server sends as part of a HTTP header in a 3xx response
func set(followLocation flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionFOLLOWLOCATION, flag ? 1 : 0).asError()
}
/// Switch off the progress meter. It will also prevent the CFURLSessionOptionPROGRESSFUNCTION from getting called.
func set(progressMeterOff flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOPROGRESS, flag ? 1 : 0).asError()
}
/// Skip all signal handling
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOSIGNAL.html
func set(skipAllSignalHandling flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOSIGNAL, flag ? 1 : 0).asError()
}
/// Set error buffer for error messages
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ERRORBUFFER.html
func set(errorBuffer buffer: UnsafeMutableBufferPointer<UInt8>?) {
let buffer = buffer ?? errorBuffer.withUnsafeMutableBufferPointer { $0 }
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionERRORBUFFER, buffer.baseAddress).asError()
}
/// Request failure on HTTP response >= 400
func set(failOnHTTPErrorCode flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionFAILONERROR, flag ? 1 : 0).asError()
}
/// URL to use in the request
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_URL.html
func set(url: URL) {
_url = url
url.absoluteString.withCString {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionURL, UnsafeMutablePointer(mutating: $0)).asError()
}
}
func set(sessionConfig config: URLSession._Configuration) {
_config = config
}
/// Set allowed protocols
///
/// - Note: This has security implications. Not limiting this, someone could
/// redirect a HTTP request into one of the many other protocols that libcurl
/// supports.
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_PROTOCOLS.html
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_REDIR_PROTOCOLS.html
func setAllowedProtocolsToHTTPAndHTTPS() {
let protocols = (CFURLSessionProtocolHTTP | CFURLSessionProtocolHTTPS)
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionPROTOCOLS, protocols).asError()
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionREDIR_PROTOCOLS, protocols).asError()
#if os(Android)
// See https://curl.haxx.se/docs/sslcerts.html
// For SSL on Android you need a "cacert.pem" to be
// accessible at the path pointed to by this env var.
// Downloadable here: https://curl.haxx.se/ca/cacert.pem
if let caInfo = getenv("URLSessionCertificateAuthorityInfoFile") {
if String(cString: caInfo) == "INSECURE_SSL_NO_VERIFY" {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionSSL_VERIFYPEER, 0).asError()
}
else {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCAINFO, caInfo).asError()
}
}
#endif
//TODO: Added in libcurl 7.45.0
//TODO: Set default protocol for schemeless URLs
//CURLOPT_DEFAULT_PROTOCOL available only in libcurl 7.45.0
}
//TODO: Proxy setting, namely CFURLSessionOptionPROXY, CFURLSessionOptionPROXYPORT,
// CFURLSessionOptionPROXYTYPE, CFURLSessionOptionNOPROXY, CFURLSessionOptionHTTPPROXYTUNNEL, CFURLSessionOptionPROXYHEADER,
// CFURLSessionOptionHEADEROPT, etc.
/// set preferred receive buffer size
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_BUFFERSIZE.html
func set(preferredReceiveBufferSize size: Int) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionBUFFERSIZE, numericCast(min(size, Int(CFURLSessionMaxWriteSize)))).asError()
}
/// Set custom HTTP headers
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
func set(customHeaders headers: [String]) {
let list = _CurlStringList(headers)
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionHTTPHEADER, list.asUnsafeMutablePointer).asError()
// We need to retain the list for as long as the rawHandle is in use.
headerList = list
}
///TODO: Wait for pipelining/multiplexing. Unavailable on Ubuntu 14.0
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_PIPEWAIT.html
//TODO: The public API does not allow us to use CFURLSessionOptionSTREAM_DEPENDS / CFURLSessionOptionSTREAM_DEPENDS_E
// Might be good to add support for it, though.
///TODO: Set numerical stream weight when CURLOPT_PIPEWAIT is enabled
/// - Parameter weight: values are clamped to lie between 0 and 1
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_STREAM_WEIGHT.html
/// - SeeAlso: http://httpwg.org/specs/rfc7540.html#StreamPriority
/// Enable automatic decompression of HTTP downloads
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTP_CONTENT_DECODING.html
func set(automaticBodyDecompression flag: Bool) {
if flag {
"".withCString {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionACCEPT_ENCODING, UnsafeMutableRawPointer(mutating: $0)).asError()
}
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHTTP_CONTENT_DECODING, 1).asError()
} else {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionACCEPT_ENCODING, nil).asError()
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHTTP_CONTENT_DECODING, 0).asError()
}
}
/// Set request method
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_CUSTOMREQUEST.html
func set(requestMethod method: String) {
method.withCString {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCUSTOMREQUEST, UnsafeMutableRawPointer(mutating: $0)).asError()
}
}
/// Download request without body
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOBODY.html
func set(noBody flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOBODY, flag ? 1 : 0).asError()
}
/// Enable data upload
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_UPLOAD.html
func set(upload flag: Bool) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionUPLOAD, flag ? 1 : 0).asError()
}
/// Set size of the request body to send
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_INFILESIZE_LARGE.html
func set(requestBodyLength length: Int64) {
try! CFURLSession_easy_setopt_int64(rawHandle, CFURLSessionOptionINFILESIZE_LARGE, length).asError()
}
func set(timeout value: Int) {
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionTIMEOUT, numericCast(value)).asError()
}
func getTimeoutIntervalSpent() -> Double {
var timeSpent = Double()
CFURLSession_easy_getinfo_double(rawHandle, CFURLSessionInfoTOTAL_TIME, &timeSpent)
return timeSpent / 1000
}
}
fileprivate func printLibcurlDebug(handle: CFURLSessionEasyHandle, type: CInt, data: UnsafeMutablePointer<Int8>, size: Int, userInfo: UnsafeMutableRawPointer?) -> CInt {
// C.f. <https://curl.haxx.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html>
let info = CFURLSessionInfo(value: type)
let text = data.withMemoryRebound(to: UInt8.self, capacity: size, {
let buffer = UnsafeBufferPointer<UInt8>(start: $0, count: size)
return String(utf8Buffer: buffer)
}) ?? "";
guard let userInfo = userInfo else { return 0 }
let task = Unmanaged<URLSessionTask>.fromOpaque(userInfo).takeUnretainedValue()
printLibcurlDebug(type: info, data: text, task: task)
return 0
}
fileprivate func printLibcurlDebug(type: CFURLSessionInfo, data: String, task: URLSessionTask) {
// libcurl sends is data with trailing CRLF which inserts lots of newlines into our output.
NSLog("[\(task.taskIdentifier)] \(type.debugHeader) \(data.mapControlToPictures)")
}
fileprivate extension String {
/// Replace control characters U+0000 - U+0019 to Control Pictures U+2400 - U+2419
var mapControlToPictures: String {
let d = self.unicodeScalars.map { (u: UnicodeScalar) -> UnicodeScalar in
switch u.value {
case 0..<0x20: return UnicodeScalar(u.value + 0x2400)!
default: return u
}
}
return String(String.UnicodeScalarView(d))
}
}
extension _EasyHandle {
/// Send and/or receive pause state for an `EasyHandle`
struct _PauseState : OptionSet {
let rawValue: Int8
init(rawValue: Int8) { self.rawValue = rawValue }
static let receivePaused = _PauseState(rawValue: 1 << 0)
static let sendPaused = _PauseState(rawValue: 1 << 1)
}
}
extension _EasyHandle._PauseState {
func setState(on handle: _EasyHandle) {
try! CFURLSessionEasyHandleSetPauseState(handle.rawHandle, contains(.sendPaused) ? 1 : 0, contains(.receivePaused) ? 1 : 0).asError()
}
}
extension _EasyHandle._PauseState : TextOutputStreamable {
func write<Target : TextOutputStream>(to target: inout Target) {
switch (self.contains(.receivePaused), self.contains(.sendPaused)) {
case (false, false): target.write("unpaused")
case (true, false): target.write("receive paused")
case (false, true): target.write("send paused")
case (true, true): target.write("send & receive paused")
}
}
}
extension _EasyHandle {
/// Pause receiving data.
///
/// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html
func pauseReceive() {
guard !pauseState.contains(.receivePaused) else { return }
pauseState.insert(.receivePaused)
pauseState.setState(on: self)
}
/// Pause receiving data.
///
/// - Note: Chances are high that delegate callbacks (with pending data)
/// will be called before this method returns.
/// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html
func unpauseReceive() {
guard pauseState.contains(.receivePaused) else { return }
pauseState.remove(.receivePaused)
pauseState.setState(on: self)
}
/// Pause sending data.
///
/// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html
func pauseSend() {
guard !pauseState.contains(.sendPaused) else { return }
pauseState.insert(.sendPaused)
pauseState.setState(on: self)
}
/// Pause sending data.
///
/// - Note: Chances are high that delegate callbacks (with pending data)
/// will be called before this method returns.
/// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html
func unpauseSend() {
guard pauseState.contains(.sendPaused) else { return }
pauseState.remove(.sendPaused)
pauseState.setState(on: self)
}
}
internal extension _EasyHandle {
/// errno number from last connect failure
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html
var connectFailureErrno: Int {
#if os(Windows) && (arch(arm64) || arch(x86_64))
var errno = Int32()
#else
var errno = Int()
#endif
try! CFURLSession_easy_getinfo_long(rawHandle, CFURLSessionInfoOS_ERRNO, &errno).asError()
return numericCast(errno)
}
}
extension CFURLSessionInfo : Equatable {
public static func ==(lhs: CFURLSessionInfo, rhs: CFURLSessionInfo) -> Bool {
return lhs.value == rhs.value
}
}
extension CFURLSessionInfo {
public var debugHeader: String {
switch self {
case CFURLSessionInfoTEXT: return " "
case CFURLSessionInfoHEADER_OUT: return "=> Send header ";
case CFURLSessionInfoDATA_OUT: return "=> Send data ";
case CFURLSessionInfoSSL_DATA_OUT: return "=> Send SSL data ";
case CFURLSessionInfoHEADER_IN: return "<= Recv header ";
case CFURLSessionInfoDATA_IN: return "<= Recv data ";
case CFURLSessionInfoSSL_DATA_IN: return "<= Recv SSL data ";
default: return " "
}
}
}
extension _EasyHandle {
/// the URL a redirect would go to
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLINFO_REDIRECT_URL.html
var redirectURL: URL? {
var p: UnsafeMutablePointer<Int8>? = nil
try! CFURLSession_easy_getinfo_charp(rawHandle, CFURLSessionInfoREDIRECT_URL, &p).asError()
guard let cstring = p else { return nil }
guard let s = String(cString: cstring, encoding: .utf8) else { return nil }
return URL(string: s)
}
}
fileprivate extension _EasyHandle {
static func from(callbackUserData userdata: UnsafeMutableRawPointer?) -> _EasyHandle? {
guard let userdata = userdata else { return nil }
return Unmanaged<_EasyHandle>.fromOpaque(userdata).takeUnretainedValue()
}
}
fileprivate extension _EasyHandle {
func resetTimer() {
//simply create a new timer with the same queue, timeout and handler
//this must cancel the old handler and reset the timer
timeoutTimer = _TimeoutSource(queue: timeoutTimer.queue, milliseconds: timeoutTimer.milliseconds, handler: timeoutTimer.handler)
}
/// Forward the libcurl callbacks into Swift methods
func setupCallbacks() {
// write
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionWRITEDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionWRITEFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
defer {
handle.resetTimer()
}
return handle.didReceive(data: data, size: size, nmemb: nmemb)
}.asError()
// read
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionREADDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionREADFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
defer {
handle.resetTimer()
}
return handle.fill(writeBuffer: data, size: size, nmemb: nmemb)
}.asError()
// header
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionHEADERDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionHEADERFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
defer {
handle.resetTimer()
}
var length = Double()
try! CFURLSession_easy_getinfo_double(handle.rawHandle, CFURLSessionInfoCONTENT_LENGTH_DOWNLOAD, &length).asError()
return handle.didReceive(headerData: data, size: size, nmemb: nmemb, contentLength: length)
}.asError()
// socket options
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSOCKOPTDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_sc(rawHandle, CFURLSessionOptionSOCKOPTFUNCTION) { (userdata: UnsafeMutableRawPointer?, fd: CInt, type: CFURLSessionSocketType) -> CInt in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
guard type == CFURLSessionSocketTypeIPCXN else { return 0 }
do {
try handle.setSocketOptions(for: fd)
return 0
} catch {
return 1
}
}.asError()
// seeking in input stream
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSEEKDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_seek(rawHandle, CFURLSessionOptionSEEKFUNCTION, { (userdata, offset, origin) -> Int32 in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return CFURLSessionSeekFail }
return handle.seekInputStream(offset: offset, origin: origin)
}).asError()
// progress
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOPROGRESS, 0).asError()
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionPROGRESSDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
try! CFURLSession_easy_setopt_tc(rawHandle, CFURLSessionOptionXFERINFOFUNCTION, { (userdata: UnsafeMutableRawPointer?, dltotal :Int64, dlnow: Int64, ultotal: Int64, ulnow: Int64) -> Int32 in
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return -1 }
handle.updateProgressMeter(with: _Progress(totalBytesSent: ulnow, totalBytesExpectedToSend: ultotal, totalBytesReceived: dlnow, totalBytesExpectedToReceive: dltotal))
return 0
}).asError()
}
/// This callback function gets called by libcurl when it receives body
/// data.
///
/// - SeeAlso: <https://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.html>
func didReceive(data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int) -> Int {
let d: Int = {
let buffer = Data(bytes: data, count: size*nmemb)
switch delegate?.didReceive(data: buffer) {
case .proceed?: return size * nmemb
case .abort?: return 0
case .pause?:
pauseState.insert(.receivePaused)
return Int(CFURLSessionWriteFuncPause)
case nil:
/* the delegate disappeared */
return 0
}
}()
return d
}
/// This callback function gets called by libcurl when it receives header
/// data.
///
/// - SeeAlso: <https://curl.haxx.se/libcurl/c/CURLOPT_HEADERFUNCTION.html>
func didReceive(headerData data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, contentLength: Double) -> Int {
let buffer = Data(bytes: data, count: size*nmemb)
let d: Int = {
switch delegate?.didReceive(headerData: buffer, contentLength: Int64(contentLength)) {
case .proceed?: return size * nmemb
case .abort?: return 0
case .pause?:
pauseState.insert(.receivePaused)
return Int(CFURLSessionWriteFuncPause)
case nil:
/* the delegate disappeared */
return 0
}
}()
setCookies(headerData: buffer)
return d
}
func setCookies(headerData data: Data) {
guard let config = _config, config.httpCookieAcceptPolicy != HTTPCookie.AcceptPolicy.never else { return }
guard let headerData = String(data: data, encoding: String.Encoding.utf8) else { return }
// Convert headerData from a string to a dictionary.
// Ignore headers like 'HTTP/1.1 200 OK\r\n' which do not have a key value pair.
// Value can have colons (ie, date), so only split at the first one, ie header:value
let headerComponents = headerData.split(separator: ":", maxSplits: 1)
var headers: [String: String] = [:]
//Trim the leading and trailing whitespaces (if any) before adding the header information to the dictionary.
if headerComponents.count > 1 {
headers[String(headerComponents[0].trimmingCharacters(in: .whitespacesAndNewlines))] = headerComponents[1].trimmingCharacters(in: .whitespacesAndNewlines)
}
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: _url!)
guard cookies.count > 0 else { return }
if let cookieStorage = config.httpCookieStorage {
cookieStorage.setCookies(cookies, for: _url, mainDocumentURL: nil)
}
}
/// This callback function gets called by libcurl when it wants to send data
/// it to the network.
///
/// - SeeAlso: <https://curl.haxx.se/libcurl/c/CURLOPT_READFUNCTION.html>
func fill(writeBuffer data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int) -> Int {
let d: Int = {
let buffer = UnsafeMutableBufferPointer(start: data, count: size * nmemb)
switch delegate?.fill(writeBuffer: buffer) {
case .pause?:
pauseState.insert(.sendPaused)
return Int(CFURLSessionReadFuncPause)
case .abort?:
return Int(CFURLSessionReadFuncAbort)
case .bytes(let length)?:
return length
case nil:
/* the delegate disappeared */
return Int(CFURLSessionReadFuncAbort)
}
}()
return d
}
func setSocketOptions(for fd: CInt) throws {
//TODO: At this point we should call setsockopt(2) to set the QoS on
// the socket based on the QoS of the request.
//
// On Linux this can be done with IP_TOS. But there's both IntServ and
// DiffServ.
//
// Not sure what Darwin uses.
//
// C.f.:
// <https://en.wikipedia.org/wiki/Type_of_service>
// <https://en.wikipedia.org/wiki/Quality_of_service>
}
func updateProgressMeter(with propgress: _Progress) {
delegate?.updateProgressMeter(with: propgress)
}
func seekInputStream(offset: Int64, origin: CInt) -> CInt {
let d: Int32 = {
/// libcurl should only use SEEK_SET
guard origin == SEEK_SET else { fatalError("Unexpected 'origin' in seek.") }
do {
if let delegate = delegate {
try delegate.seekInputStream(to: UInt64(offset))
return CFURLSessionSeekOk
} else {
return CFURLSessionSeekCantSeek
}
} catch {
return CFURLSessionSeekCantSeek
}
}()
return d
}
}
extension _EasyHandle {
/// The progress of a transfer.
///
/// The number of bytes that we expect to download and upload, and the
/// number of bytes downloaded and uploaded so far.
///
/// Unknown values will be set to zero. E.g. if the number of bytes
/// expected to be downloaded is unknown, `totalBytesExpectedToReceive`
/// will be zero.
struct _Progress {
let totalBytesSent: Int64
let totalBytesExpectedToSend: Int64
let totalBytesReceived: Int64
let totalBytesExpectedToReceive: Int64
}
}
extension _EasyHandle {
/// A simple wrapper / helper for libcurl’s `slist`.
///
/// It's libcurl's way to represent an array of strings.
internal class _CurlStringList {
fileprivate var rawList: OpaquePointer? = nil
init() {}
init(_ strings: [String]) {
strings.forEach { append($0) }
}
deinit {
CFURLSessionSListFreeAll(rawList)
}
}
}
extension _EasyHandle._CurlStringList {
func append(_ string: String) {
string.withCString {
rawList = CFURLSessionSListAppend(rawList, $0)
}
}
var asUnsafeMutablePointer: UnsafeMutableRawPointer? {
return rawList.map{ UnsafeMutableRawPointer($0) }
}
}
extension CFURLSessionEasyCode : Equatable {
public static func ==(lhs: CFURLSessionEasyCode, rhs: CFURLSessionEasyCode) -> Bool {
return lhs.value == rhs.value
}
}
extension CFURLSessionEasyCode : Error {
public var _domain: String { return "libcurl.Easy" }
public var _code: Int { return Int(self.value) }
}
internal extension CFURLSessionEasyCode {
func asError() throws {
if self == CFURLSessionEasyCodeOK { return }
throw self
}
}