Awesome
Perfect-CURL
<p align="center"> <a href="http://perfect.org/get-involved.html" target="_blank"> <img src="http://perfect.org/assets/github/perfect_github_2_0_0.jpg" alt="Get Involed with Perfect!" width="854" /> </a> </p> <p align="center"> <a href="https://github.com/PerfectlySoft/Perfect" target="_blank"> <img src="http://www.perfect.org/github/Perfect_GH_button_1_Star.jpg" alt="Star Perfect On Github" /> </a> <a href="http://stackoverflow.com/questions/tagged/perfect" target="_blank"> <img src="http://www.perfect.org/github/perfect_gh_button_2_SO.jpg" alt="Stack Overflow" /> </a> <a href="https://twitter.com/perfectlysoft" target="_blank"> <img src="http://www.perfect.org/github/Perfect_GH_button_3_twit.jpg" alt="Follow Perfect on Twitter" /> </a> <a href="http://perfect.ly" target="_blank"> <img src="http://www.perfect.org/github/Perfect_GH_button_4_slack.jpg" alt="Join the Perfect Slack" /> </a> </p> <p align="center"> <a href="https://developer.apple.com/swift/" target="_blank"> <img src="https://img.shields.io/badge/Swift-4.0-orange.svg?style=flat" alt="Swift 4.0"> </a> <a href="https://developer.apple.com/swift/" target="_blank"> <img src="https://img.shields.io/badge/Platforms-OS%20X%20%7C%20Linux%20-lightgray.svg?style=flat" alt="Platforms OS X | Linux"> </a> <a href="http://perfect.org/licensing.html" target="_blank"> <img src="https://img.shields.io/badge/License-Apache-lightgrey.svg?style=flat" alt="License Apache"> </a> <a href="http://twitter.com/PerfectlySoft" target="_blank"> <img src="https://img.shields.io/badge/Twitter-@PerfectlySoft-blue.svg?style=flat" alt="PerfectlySoft Twitter"> </a> <a href="http://perfect.ly" target="_blank"> <img src="http://perfect.ly/badge.svg" alt="Slack Status"> </a> </p>This package provides support for curl in Swift. This package builds with Swift Package Manager and is part of the Perfect project.
Building
Ensure you have installed and activated the latest Swift 4.0+ tool chain.
Add this project as a dependency in your Package.swift file.
.Package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", majorVersion: 3)
Linux Build Notes
Ensure that you have installed libcurl.
sudo apt-get install libcurl4-openssl-dev
Usage
import PerfectCURL
This package uses a simple request/response model to access URL contents. Start by creating a CURLRequest
object and configure it according to your needs, then ask it to perform the request and return a response. Responses are represented by CURLResponse
objects.
Requests can be executed either synchronously - blocking the calling thread until the request completes, asynchronously - delivering the response to a callback, or through a Promise object - performing the request on a background thread giving you a means to chain additional tasks, poll, or wait for completion.
Creating Requests
CURLRequest
objects can be created with a URL and a series of options, or they can be created blank and then fully configured. CURLRequest
provides the following initializers:
open class CURLRequest {
// Init with a url and options array.
public convenience init(_ url: String, options: [Option] = [])
/// Init with url and one or more options.
public convenience init(_ url: String, _ option1: Option, _ options: Option...)
/// Init with array of options.
public init(options: [Option] = [])
}
Options can be provided using either Array<Option>
or variadic parameters. Options can also be directly added to the CURLRequest.options
property before the request is executed.
Configuring Requests
CURLRequest
options are represented by the CURLRequest.Option
enum. Each enum case will have zero or more associated values which indicate the parameters for the particular option. For example, the URL for the request could be indicated with the option .url("https://httpbin.org/post")
. Most of the options that curl makes available are represented in the CURLRequest.Option
enum. The full list of available options is presented near the end of this document.
POST Data
POST field data, including file uploads, are added to request the same way other options are. The .postField(POSTField)
, .postData([UInt8])
, and .postString(String)
enum cases will set the request's POST content. The .postField
case can be added to a request multiple times as each instance represents one set of name/value pair. The .postData
and .postString
cases should be considered mutually exclusive to other post cases as adding either will overwrite any previously set POST content. Adding POST content data of any sort will automatically set the HTTP method to POST.
The CURLRequest.POSTField
struct is defined as follows:
open class CURLRequest {
public struct POSTField {
/// Init with a name, value and optional mime-type.
public init(name: String, value: String, mimeType: String? = nil)
/// Init with a name, file path and optional mime-type.
public init(name: String, filePath: String, mimeType: String? = nil)
}
}
The example below creates a POST request and adds several name/value pairs as well as a file. The executed request will automatically have a "multipart/form-data" content type.
let json = try CURLRequest(url, .failOnError,
.postField(.init(name: "key1", value: "value1")),
.postField(.init(name: "key2", value: "value2")),
.postField(.init(name: "file1", filePath: testFile.path, mimeType: "text/plain")))
.perform().bodyJSON
Fetching Responses
To perform a request, call one of the CURLRequest.perform
or CURLRequest.promise
functions. If the request is successful then you will be provided a CURLResponse
object which can be used to get response data. If the request fails then a CURLResponse.Error
will be thrown. A request may fail if it is unable to connect, times out, receives a malformed response, or receives a HTTP response with a status code equal to or greater than 400 when the .failOnError
option is given. If the .failOnError
option is not given then any valid HTTP response will be a success, regardless of the response status code.
The three functions for obtaining a response are as follows:
public extension CURLRequest {
/// Execute the request synchronously.
/// Returns the response or throws an Error.
func perform() throws -> CURLResponse
/// Execute the request asynchronously.
/// The parameter passed to the completion callback must be called to obtain the response or throw an Error.
func perform(_ completion: @escaping (CURLResponse.Confirmation) -> ())
/// Execute the request asynchronously.
/// Returns a Promise object which can be used to monitor the operation.
func promise() -> Promise<CURLResponse>
}
The first CURLRequest.perform
function executes the request synchronously on the calling thread. The function call will block until the request succeeds or fails. On failure, a CURLResponse.Error
will be thrown.
The second CURLRequest.perform
function executes the request asynchronously on background threads as necessary. The parameter passed to this function is a callback which will be given a CURLResponse.Confirmation
once the request completes or fails. Calling the confirmation parameter from within your callback will either return the CURLResponse
or throw a CURLResponse.Error
.
The third function, CURLRequest.perform
, will return a Promise<CURLResponse>
object which can be used to chain further activities and poll or wait for the request to complete. As with the other response generating functions, a CURLResponse.Error
will be thrown if an error occurs. Information on the Promise object in general can be found in the Perfect-Thread documentation.
The following three example shows how each of the functions are used. Each will perform a request and convert the resulting response body from JSON into a [String:Any] dictionary.
• Synchronously fetch an API endpoint and decode it from JSON:
let url = "https://httpbin.org/get?p1=v1&p2=v2"
let json: [String:Any] = try CURLRequest(url).perform().bodyJSON
• Asynchronously fetch an API endpoint and decode it from JSON:
let url = "https://httpbin.org/post"
CURLRequest(url).perform {
confirmation in
do {
let response = try confirmation()
let json: [String:Any] = response.bodyJSON
} catch let error as CURLResponse.Error {
print("Failed: response code \(error.response.responseCode)")
} catch {
print("Fatal error \(error)")
}
}
• Asynchronously fetch an API endpoint using a Promise and decode it from JSON:
let url = "https://httpbin.org/get?p1=v1&p2=v2"
if let json = try CURLRequest(url).promise().then { return try $0().bodyJSON }.wait() {
...
}
The three available functions ranked according to efficiency would be ordered as:
- Asynchronous
perform
- Asynchronous
promise
- Synchronous
perform
When performing CURL requests on a high-traffic server it is advised that one of the asynchronous response functions be used.
Reset Request
A CURLRequest
object can be reused for subsequent connections by calling the .reset
function. Resetting a request will clear out any previously set options, including the target URL. The .reset
function accepts as an optional parameter new options with which the request will be reconfigured.
The reset declaration follows:
public extension CURLRequest {
/// Reset the request. Clears all options so that the object can be reused.
/// New options can be provided.
func reset(_ options: [Option] = [])
/// Reset the request. Clears all options so that the object can be reused.
/// New options can be provided.
func reset(_ option: Option, _ options: Option...)
}
Resetting a request will invalidate any previously executed CURLResponse
objects. The reconfigured request should be reexecuted to obtain an updated CURLResponse
.
Response Data
A CURLResponse
object provides access to the response's content body as either raw bytes, a String or as a JSON decoded [String:Any] dictionary. In addition, meta-information such as the response HTTP headers and status code can be retrieved.
Response body data is made available through a series of get-only CURLResponse
properties:
public extension CURLResponse {
/// The response's raw content body bytes.
public var bodyBytes: [UInt8]
/// Get the response body converted from UTF-8.
public var bodyString: String
/// Get the response body decoded from JSON into a [String:Any] dictionary.
/// Invalid/non-JSON body data will result in an empty dictionary being returned.
public var bodyJSON: [String:Any]
/// Get the response body decoded from JSON into a decodable structure
/// Invalid/non-JSON body data will throw errors.
public func bodyJSON<T: Decodable>(_ type: T.Type) throws -> T
}
The remaining response data can be retrieved by calling one of the CURLResponse.get
functions and passing in an enum value corresponding to the desired data. The enums indicating these values are separated into three groups, each according to the type of data that would be returned; one of String, Int or Double. The enum types are CURLResponse.Info.StringValue
, CURLResponse.Info.IntValue
, and CURLResponse.Info.DoubleValue
. In addition, get
functions are provided for directly pulling header values from the response.
public extension CURLResponse {
/// Get an response info String value.
func get(_ stringValue: Info.StringValue) -> String?
/// Get an response info Int value.
func get(_ intValue: Info.IntValue) -> Int?
/// Get an response info Double value.
func get(_ doubleValue: Info.DoubleValue) -> Double?
/// Get a response header value. Returns the first found instance or nil.
func get(_ header: Header.Name) -> String?
/// Get a response header's values. Returns all found instances.
func get(all header: Header.Name) -> [String]
}
For convenience properties have been added for pulling commonly requested data from a response such as url
and responseCode
.
The following examples show how to pull header and other meta-data from the request:
// get the response code
let code = response.get(.responseCode)
// get the response code using the accessor
let code = response.responseCode
// get the "Last-Modified" header from the response
if let lastMod = response.get(.lastModified) {
...
}
Failures
When a failure occurs a CURLResponse.Error
object will be thrown. This object provides the CURL error code which was generated (not that this is different from any HTTP response code and is CURL specific). It also provides access to an error message string, and a CURLResponse object which can be used to further inquire about the resulting error.
Note that, depending on the options which were set on the request, the response object obtained after an error may not have any associated content body data.
CURLResponse.Error
is defined as follows:
open class CURLResponse {
/// An error thrown while retrieving a response.
public struct Error: Swift.Error {
/// The curl specific request response code.
public let code: Int
/// The string message for the curl response code.
public let description: String
/// The response object for this error.
public let response: CURLResponse
}
}
CURLRequest.Option List
The following is a list of the numerous CURLRequest
options which can be set. Each enum case indicates the parameter types for the option. These enum values can be used when creating a new CURLRequest
object or by adding them to an existing object's .options
array property.
CURLRequest.Option enum case | Description |
---|---|
.url(String) | The URL for the request. |
.port(Int) | Override the port for the request. |
.failOnError | Fail on http error codes >= 400. |
.userPwd(String) | Colon separated username/password string. |
.proxy(String) | Proxy server address. |
.proxyUserPwd(String) | Proxy server username/password combination. |
.proxyPort(Int) | Port override for the proxy server. |
.timeout(Int) | Maximum time in seconds for the request to complete. The default timeout is never. |
.connectTimeout(Int) | Maximum time in seconds for the request connection phase. The default timeout is 300 seconds. |
.lowSpeedLimit(Int) | The average transfer speed in bytes per second that the transfer should be below during .lowSpeedLimit seconds for the request to be too slow and abort. |
.lowSpeedTime(Int) | The time in seconds that the transfer speed should be below the .lowSpeedLimit for therequest to be considered too slow and aborted. |
.range(String) | Range request value as a string in the format "X-Y", where either X or Y may be left out and X and Y are byte indexes |
.resumeFrom(Int) | The offset in bytes at which the request should start form. |
.cookie(String) | Set one or more cookies for the request. Should be in the format "name=value". Separate multiple cookies with a semi-colon: "name1=value1; name2=value2". |
.cookieFile(String) | The name of the file holding cookie data for the request. |
.cookieJar(String) | The name opf the file to which received cookies will be written. |
.followLocation(Bool) | Indicated that the request should follow redirects. Default is false. |
.maxRedirects(Int) | Maximum number of redirects the request should follow. Default is unlimited. |
.maxConnects(Int) | Maximum number of simultaneously open persistent connections that may cached for the request. |
.autoReferer(Bool) | When enabled, the request will automatically set the Referer: header field in HTTP requests when it follows a Location: redirect |
.krbLevel(KBRLevel) | Sets the kerberos security level for FTP. Value should be one of the following: .clear, .safe, .confidential or .private. |
.addHeader(Header.Name, String) | Add a header to the request. |
.addHeaders([(Header.Name, String)]) | Add a series of headers to the request. |
.replaceHeader(Header.Name, String) | Add or replace a header. |
.removeHeader(Header.Name) | Remove a default internally added header. |
.sslCert(String) | Path to the client SSL certificate. |
.sslCertType(SSLFileType) | Specifies the type for the client SSL certificate. Defaults to .pem . |
.sslKey(String) | Path to client private key file. |
.sslKeyPwd(String) | Password to be used if the SSL key file is password protected. |
.sslKeyType(SSLFileType) | Specifies the type for the SSL private key file. |
.sslVersion(TLSMethod) | Force the request to use a specific version of TLS or SSL. |
.sslVerifyPeer(Bool) | Inticates whether the request should verify the authenticity of the peer's certificate. |
.sslVerifyHost(Bool) | Indicates whether the request should verify that the server cert is for the server it is known as. |
.sslCAFilePath(String) | Path to file holding one or more certificates which will be used to verify the peer. |
.sslCADirPath(String) | Path to directory holding one or more certificates which will be used to verify the peer. |
.sslCiphers([String]) | Override the list of ciphers to use for the SSL connection. Consists of one or more cipher strings separated by colons. Commas or spaces are also acceptable separators but colons are normally used. "!", "-" and "+" can be used as operators. |
.sslPinnedPublicKey(String) | File path to the pinned public key. When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. A public key is extracted from this certificate and if it does not exactly match the public key provided to this option, curl will abort the connection before sending or receiving any data. |
.ftpPreCommands([String]) | List of (S)FTP commands to be run before the file transfer. |
.ftpPostCommands([String]) | List of (S)FTP commands to be run after the file transfer. |
.ftpPort(String) | Specifies the local connection port for active FTP transfers. |
.ftpResponseTimeout(Int) | The time in seconds that the request will wait for FTP server responses. |
.sshPublicKey(String) | Path to the public key file used for SSH connections. |
.sshPrivateKey(String) | Path to the private key file used for SSH connections. |
.httpMethod(HTTPMethod) | HTTP method to be used for the request. |
.postField(POSTField) | Adds a single POST field to the request. Generally, multiple POSt fields are added for a request. |
.postData([UInt8]) | Raw bytes to be used for a POST request. |
.postString(String) | Raw string data to be used for a POST request. |
.mailFrom(String) | Specifies the sender's address when performing an SMTP request. |
.mailRcpt(String) | Specifies the recipient when performing an SMTP request. Multiple recipients may be specified by using this option multiple times. |
CURLResponse.Info List
The lists which follow describe the CURLResponse.Info
cases which are used with the CURLResponse.get
function to retrieve response information. The lists are grouped according to the type of data which would be returned; StringValue
, IntValue
, and DoubleValue
, respectively.
CURLResponse.Info.StringValue enum case | Description |
---|---|
.url | The effective URL for the request/response. This is ultimately the URL from which the response data came from. This may differ from the request's URL in the case of a redirect. |
.ftpEntryPath | The initial path that the request ended up at after logging in to the FTP server. |
.redirectURL | The URL that the request would have been redirected to. |
.localIP | The local IP address that the request used most recently. |
.primaryIP | The remote IP address that the request most recently connected to. |
.contentType | The content type for the request. This is read from the "Content-Type" header. |
CURLResponse.Info.IntValue enum case | Description |
---|---|
.responseCode | The last received HTTP, FTP or SMTP response code. |
.headerSize | The total size in bytes of all received headers. |
.requestSize | The total size of the issued request in bytes. This will indicate the cumulative total of all requests sent in the case of a redirect. |
.sslVerifyResult | The result of the SSL certificate verification. |
.redirectCount | The total number of redirections that were followed. |
.httpConnectCode | The last received HTTP proxy response code to a CONNECT request. |
.osErrno | The OS level errno which may have triggered a failure. |
.numConnects | The number of connections that the request had to make in order to produce a response. |
.primaryPort | The remote port that the request most recently connected to |
.localPort | The local port that the request used most recently |
CURLResponse.Info.DoubleValue enum case | Description |
---|---|
.totalTime | The total time in seconds for the previous request. |
.nameLookupTime | The total time in seconds from the start until the name resolving was completed. |
.connectTime | The total time in seconds from the start until the connection to the remote host or proxy was completed. |
.preTransferTime | The time, in seconds, it took from the start until the file transfer is just about to begin. |
.sizeUpload | The total number of bytes uploaded. |
.sizeDownload | The total number of bytes downloaded. |
.speedDownload | The average download speed measured in bytes/second. |
.speedUpload | The average upload speed measured in bytes/second. |
.contentLengthDownload | The content-length of the download. This value is obtained from the Content-Length header field. |
.contentLengthUpload | The specified size of the upload. |
.startTransferTime | The time, in seconds, it took from the start of the request until the first byte was received. |
.redirectTime | The total time, in seconds, it took for all redirection steps include name lookup, connect, pretransfer and transfer before final transaction was started. |
.appConnectTime | The time, in seconds, it took from the start until the SSL/SSH connect/handshake to the remote host was completed. |
Further Information
For more information on the Perfect project, please visit perfect.org.