diff --git a/Package.swift b/Package.swift index 2c2a0ea..bb1f844 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,6 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", from: "3.1.0"), .package(url: "https://github.com/vapor/vapor.git", from: "3.1.0"), ], targets: [ @@ -22,7 +20,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "CurlyClient", - dependencies: ["PerfectCURL", "Vapor"]), + dependencies: ["CCurl", "Vapor"]), + .systemLibrary(name: "CCurl", pkgConfig: "libcurl"), .testTarget( name: "CurlyClientTests", dependencies: ["CurlyClient"]), diff --git a/README.md b/README.md index 79545c8..2cca5b7 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,6 @@ public func configure(_ config: inout Config, _ env: inout Environment, _ servic } ``` -### 3. (Linux only) Check system dependencies - -If you're building for Linux, you need to have the `uuid-dev` package installed – a requirement of Perfect-LinuxBridge. If you are building a thin Docker image, be sure to install `libuuid1` in the final build. - -### 4. Profit! +### 3. Profit! Your Vapor app should now use curl directly instead of URLSession. diff --git a/Sources/CCurl/ccurl.h b/Sources/CCurl/ccurl.h new file mode 100644 index 0000000..1df51dc --- /dev/null +++ b/Sources/CCurl/ccurl.h @@ -0,0 +1,114 @@ + +#ifndef _p_curl_h_ +#define _p_curl_h_ + +#include + +#ifdef CURLOPT_HEADERDATA +#undef CURLOPT_HEADERDATA +CURLoption CURLOPT_HEADERDATA = CURLOPT_WRITEHEADER; +#endif +#ifdef CURLOPT_WRITEDATA +#undef CURLOPT_WRITEDATA +CURLoption CURLOPT_WRITEDATA = CURLOPT_FILE; +#endif +#ifdef CURLOPT_READDATA +#undef CURLOPT_READDATA +CURLoption CURLOPT_READDATA = CURLOPT_INFILE; +#endif + +typedef size_t (*curl_func)(void * ptr, size_t size, size_t num, void * ud); + +static CURLcode curl_easy_setopt_long(CURL *handle, CURLoption option, long value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_setopt_cstr(CURL *handle, CURLoption option, const char * value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_setopt_int64(CURL *handle, CURLoption option, long long value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_setopt_slist(CURL *handle, CURLoption option, struct curl_slist * value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_setopt_void(CURL *handle, CURLoption option, void * value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_setopt_func(CURL *handle, CURLoption option, curl_func value) +{ + return curl_easy_setopt(handle, option, value); +} + +static CURLcode curl_easy_getinfo_long(CURL *handle, CURLINFO option, long * value) +{ + return curl_easy_getinfo(handle, option, value); +} + +static CURLcode curl_easy_getinfo_cstr(CURL *handle, CURLINFO option, const char ** value) +{ + return curl_easy_getinfo(handle, option, value); +} + +static CURLcode curl_easy_getinfo_double(CURL *handle, CURLINFO option, double * value) +{ + return curl_easy_getinfo(handle, option, value); +} + +static CURLcode curl_easy_getinfo_slist(CURL *handle, CURLINFO option, struct curl_slist ** value) +{ + return curl_easy_getinfo(handle, option, value); +} + +static CURLcode curl_get_msg_result(CURLMsg * msg) +{ + return msg->data.result; +} + +static CURLFORMcode curl_formadd_content(struct curl_httppost **firstitem, struct curl_httppost **lastitem, + const char * name, const char * content, const long size, const char * type) { + return type ? + curl_formadd(firstitem, lastitem, + CURLFORM_COPYNAME, name, + CURLFORM_COPYCONTENTS, content, + CURLFORM_CONTENTSLENGTH, size, + CURLFORM_CONTENTTYPE, type, + CURLFORM_END) + : + curl_formadd(firstitem, lastitem, + CURLFORM_COPYNAME, name, + CURLFORM_COPYCONTENTS, content, + CURLFORM_CONTENTSLENGTH, size, + CURLFORM_END); +} + +static CURLFORMcode curl_formadd_file(struct curl_httppost **firstitem, struct curl_httppost **lastitem, + const char * name, const char * path, const char * type) { + return type ? + curl_formadd(firstitem, lastitem, + CURLFORM_COPYNAME, name, + CURLFORM_CONTENTTYPE, type, + CURLFORM_FILE, path, + CURLFORM_END) + : + curl_formadd(firstitem, lastitem, + CURLFORM_COPYNAME, name, + CURLFORM_FILE, path, + CURLFORM_END); +} + +static CURLcode curl_form_post(CURL * handle, struct curl_httppost * post) { + return curl_easy_setopt(handle, CURLOPT_HTTPPOST, post); +} + + +#endif diff --git a/Sources/CCurl/module.modulemap b/Sources/CCurl/module.modulemap new file mode 100644 index 0000000..d938b71 --- /dev/null +++ b/Sources/CCurl/module.modulemap @@ -0,0 +1,6 @@ +module CCurl +{ + umbrella header "ccurl.h" + link "curl" + export * +} diff --git a/Sources/CurlyClient/CURLRequest.swift b/Sources/CurlyClient/CURLRequest.swift new file mode 100755 index 0000000..db383d3 --- /dev/null +++ b/Sources/CurlyClient/CURLRequest.swift @@ -0,0 +1,292 @@ +// +// CURLRequest.swift +// PerfectCURL +// +// Created by Kyle Jessup on 2017-05-10. +// Copyright (C) 2017 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import CCurl + +enum TLSMethod { + case tlsV1 + case tlsV1_1 + case tlsV1_2 +} + +protocol CURLRequestBodyGenerator { + var contentLength: Int? { get } + mutating func next(byteCount: Int) -> [UInt8]? +} + +/// Creates and configures a CURL request. +/// init with a URL and zero or more options. +/// Call .perform to get the CURLResponse +open class CURLRequest { + typealias POSTFields = CURL.POSTFields + /// A header which can be added to the request. + typealias Header = HTTPRequestHeader + /// A POST name/value field. Can indicate a file upload by giving a file path. + struct POSTField { + enum FieldType { case value, file } + let name: String + let value: String + let mimeType: String? + let type: FieldType + /// Init with a name, value and optional mime-type. + init(name: String, value: String, mimeType: String? = nil) { + self.name = name + self.value = value + self.type = .value + self.mimeType = mimeType + } + /// Init with a name, file path and optional mime-type. + init(name: String, filePath: String, mimeType: String? = nil) { + self.name = name + self.value = filePath + self.type = .file + self.mimeType = mimeType + } + } + /// Kerberos security level for FTP requests. Used with `.kbrLevel` option. + enum KBRLevel { + case clear, safe, confidential, `private` + var description: String { + switch self { + case .clear: return "clear" + case .safe: return "safe" + case .confidential: return "confidential" + case .private: return "private" + } + } + } + /// SSL certificate format. Used with `.sslCertType` option. + /// The `.eng` case indicates that the `.sslCertType` value should be passed directly to the crypto engine (usually OpenSSL). + enum SSLFileType { + case pem, der, p12, eng + var description: String { + switch self { + case .pem: return "PEM" + case .der: return "DER" + case .p12: return "P12" + case .eng: return "ENG" + } + } + } + + /// The numerous options which can be set. Each enum case indicates the parameter type(s) for the option. + enum Option { + case + /// The URL for the request. + url(String), + /// Override the port for the request. + port(Int), + /// Fail on http error codes >= 400. + failOnError, + /// Colon separated username/password string. + userPwd(String), + /// Proxy server address. + proxy(String), + /// Proxy server username/password combination. + proxyUserPwd(String), + /// Port override for the proxy server. + proxyPort(Int), + /// Maximum time in seconds for the request to complete. + /// The default timeout is never. + timeout(Int), + /// Maximum time in seconds for the request connection phase. + /// The default timeout is 300 seconds. + connectTimeout(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. + lowSpeedLimit(Int), + /// The time in seconds that the transfer speed should be below the `.lowSpeedLimit` + /// for therequest to be considered too slow and aborted. + lowSpeedTime(Int), + /// 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 + range(String), + /// The offset in bytes at which the request should start form. + resumeFrom(Int), + /// 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". + cookie(String), + /// The name of the file holding cookie data for the request. + cookieFile(String), + /// The name opf the file to which received cookies will be written. + cookieJar(String), + /// Indicated that the request should follow redirects. Default is false. + followLocation(Bool), + /// Maximum number of redirects the request should follow. Default is unlimited. + maxRedirects(Int), + /// Maximum number of simultaneously open persistent connections that may cached for the request. + maxConnects(Int), + /// When enabled, the request will automatically set the Referer: header field in HTTP + /// requests when it follows a Location: redirect + autoReferer(Bool), + /// Sets the kerberos security level for FTP. + /// Value should be one of the following: .clear, .safe, .confidential or .private. + krbLevel(KBRLevel), + /// Add a header to the request. + addHeader(Header.Name, String), + /// Add a series of headers to the request. + addHeaders([(Header.Name, String)]), + /// Add or replace a header. + replaceHeader(Header.Name, String), + /// Remove a default internally added header. + removeHeader(Header.Name), + /// Set the Accept-Encoding header and enable decompression of response data. + acceptEncoding(String), + /// Path to the client SSL certificate. + sslCert(String), + /// Specifies the type for the client SSL certificate. Defaults to `.pem`. + sslCertType(SSLFileType), + /// Path to client private key file. + sslKey(String), + /// Password to be used if the SSL key file is password protected. + sslKeyPwd(String), + /// Specifies the type for the SSL private key file. + sslKeyType(SSLFileType), + /// Force the request to use a specific version of TLS or SSL. + sslVersion(TLSMethod), + /// Inticates whether the request should verify the authenticity of the peer's certificate. + sslVerifyPeer(Bool), + /// Indicates whether the request should verify that the server cert is for the server it is known as. + sslVerifyHost(Bool), + /// Path to file holding one or more certificates which will be used to verify the peer. + sslCAFilePath(String), + /// Path to directory holding one or more certificates which will be used to verify the peer. + sslCADirPath(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. + sslCiphers([String]), + /// File path to the pinned key. + /// When negotiating a TLS or SSL connection, the server sends a certificate indicating its + /// identity. A key is extracted from this certificate and if it does not exactly + /// match the key provided to this option, curl will abort the connection before + /// sending or receiving any data. + sslPinnedPublicKey(String), + /// List of (S)FTP commands to be run before the file transfer. + ftpPreCommands([String]), + /// List of (S)FTP commands to be run after the file transfer. + ftpPostCommands([String]), + /// Specifies the local connection port for active FTP transfers. + ftpPort(String), + /// The time in seconds that the request will wait for FTP server responses. + ftpResponseTimeout(Int), + /// Path to the key file used for SSH connections. + sshPublicKey(String), + /// Path to the private key file used for SSH connections. + sshPrivateKey(String), + /// HTTP method to be used for the request. + httpMethod(HTTPMethod), + /// Adds a single POST field to the request. Generally, multiple POSt fields are added for a request. + postField(POSTField), + /// Raw bytes to be used for a POST request. + postData([UInt8]), + /// Raw string data to be used for a POST request. + postString(String), + /// Specifies the sender's address when performing an SMTP request. + mailFrom(String), + /// Specifies the recipient when performing an SMTP request. + /// Multiple recipients may be specified by using this option multiple times. + mailRcpt(String), + /// CURL verbose mode. + verbose, + /// Include headers in response body. + header, + /// This connection will be using SSL. (when is this needed? SMTP only?) + useSSL, + /// Indicate that the request will be an upload. + /// And provide an object to incrementally provide the content. + upload(CURLRequestBodyGenerator) + } + + let curl: CURL + /// Mutable options array for the request. These options are cleared when the request is .reset() + var options: [Option] + var postFields: POSTFields? + var uploadBodyGen: CURLRequestBodyGenerator? + /// Init with a url and options array. + convenience init(_ url: String, options: [Option] = []) { + self.init(options: [.url(url)] + options) + } + /// Init with url and one or more options. + convenience init(_ url: String, _ option1: Option, _ options: Option...) { + self.init(options: [.url(url)] + [option1] + options) + } + /// Init with array of options. + init(options: [Option] = []) { + curl = CURL() + self.options = options + } + func applyOptions() { + options.forEach { $0.apply(to: self) } + if let postFields = self.postFields { + curl.formAddPost(fields: postFields) + } + } +} + +extension CURLRequest { + /// Execute the request synchronously. + /// Returns the response or throws an Error. + func perform() throws -> CURLResponse { + applyOptions() + let resp = CURLResponse(curl, postFields: postFields) + try resp.complete() + return resp + } + + /// 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) -> ()) { + applyOptions() + CURLResponse(curl, postFields: postFields).complete(completion) + } + + /// Reset the request. Clears all options so that the object can be reused. + /// New options can be provided. + func reset(_ options: [Option] = []) { + curl.reset() + postFields = nil + uploadBodyGen = nil + self.options = options + } + + /// Reset the request. Clears all options so that the object can be reused. + /// New options can be provided. + func reset(_ option: Option, _ options: Option...) { + reset([option] + options) + } +} + +extension CURLRequest { + /// Add a header to the response. + /// No check for duplicate or repeated headers will be made. + func addHeader(_ named: Header.Name, value: String) { + options.append(.addHeader(named, value)) + } + /// Set the indicated header value. + /// If the header already exists then the existing value will be replaced. + func replaceHeader(_ named: Header.Name, value: String) { + options.append(.replaceHeader(named, value)) + } + /// Remove the indicated header. + func removeHeader(_ named: Header.Name) { + options.append(.removeHeader(named)) + } +} + diff --git a/Sources/CurlyClient/CURLRequestOptions.swift b/Sources/CurlyClient/CURLRequestOptions.swift new file mode 100755 index 0000000..c58161c --- /dev/null +++ b/Sources/CurlyClient/CURLRequestOptions.swift @@ -0,0 +1,194 @@ +// +// CURLRequestOptions.swift +// PerfectCURL +// +// Created by Kyle Jessup on 2017-05-17. +// Copyright (C) 2017 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import CCurl + +extension CURLRequest.Option { + + private func headerAdd(_ curl: CURL, optName: CURLRequest.Header.Name, optValue: String) { + if optValue.isEmpty { + curl.setOption(CURLOPT_HTTPHEADER, s: "\(optName.standardName);") + } else { + curl.setOption(CURLOPT_HTTPHEADER, s: "\(optName.standardName): \(optValue)") + } + } + + func apply(to request: CURLRequest) { + let curl = request.curl + switch self { + case .url(let optString): + curl.setOption(CURLOPT_URL, s: optString) + case .port(let optInt): + curl.setOption(CURLOPT_PORT, int: optInt) + case .failOnError: + curl.setOption(CURLOPT_FAILONERROR, int: 1) + case .userPwd(let optString): + curl.setOption(CURLOPT_USERPWD, s: optString) + case .proxy(let optString): + curl.setOption(CURLOPT_PROXY, s: optString) + case .proxyUserPwd(let optString): + curl.setOption(CURLOPT_PROXYUSERPWD, s: optString) + case .proxyPort(let optInt): + curl.setOption(CURLOPT_PROXYPORT, int: optInt) + case .timeout(let optInt): + curl.setOption(CURLOPT_TIMEOUT, int: optInt) + case .connectTimeout(let optInt): + curl.setOption(CURLOPT_CONNECTTIMEOUT, int: optInt) + case .lowSpeedLimit(let optInt): + curl.setOption(CURLOPT_LOW_SPEED_LIMIT, int: optInt) + case .lowSpeedTime(let optInt): + curl.setOption(CURLOPT_LOW_SPEED_TIME, int: optInt) + case .range(let optString): + curl.setOption(CURLOPT_RANGE, s: optString) + case .resumeFrom(let optInt): + curl.setOption(CURLOPT_RESUME_FROM_LARGE, int: Int64(optInt)) + case .cookie(let optString): + curl.setOption(CURLOPT_COOKIE, s: optString) + case .cookieFile(let optString): + curl.setOption(CURLOPT_COOKIEFILE, s: optString) + case .cookieJar(let optString): + curl.setOption(CURLOPT_COOKIEJAR, s: optString) + case .followLocation(let optBool): + curl.setOption(CURLOPT_FOLLOWLOCATION, int: optBool ? 1 : 0) + case .maxRedirects(let optInt): + curl.setOption(CURLOPT_MAXREDIRS, int: optInt) + case .maxConnects(let optInt): + curl.setOption(CURLOPT_MAXCONNECTS, int: optInt) + case .autoReferer(let optBool): + curl.setOption(CURLOPT_AUTOREFERER, int: optBool ? 1 : 0) + case .krbLevel(let optString): + curl.setOption(CURLOPT_KRBLEVEL, s: optString.description) + case .addHeader(let optName, let optValue): + headerAdd(curl, optName: optName, optValue: optValue) + case .addHeaders(let optArray): + optArray.forEach { self.headerAdd(curl, optName: $0, optValue: $1) } + case .replaceHeader(let optName, let optValue): + curl.setOption(CURLOPT_HTTPHEADER, s: "\(optName.standardName):") + headerAdd(curl, optName: optName, optValue: optValue) + case .removeHeader(let optName): + curl.setOption(CURLOPT_HTTPHEADER, s: "\(optName.standardName):") + case .useSSL: + curl.setOption(CURLOPT_USE_SSL, int: Int(CURLUSESSL_ALL.rawValue)) + case .sslCert(let optString): + curl.setOption(CURLOPT_SSLCERT, s: optString) + case .sslCertType(let optString): + curl.setOption(CURLOPT_SSLCERTTYPE, s: optString.description) + case .sslKey(let optString): + curl.setOption(CURLOPT_SSLKEY, s: optString) + case .sslKeyPwd(let optString): + curl.setOption(CURLOPT_KEYPASSWD, s: optString) + case .sslKeyType(let optString): + curl.setOption(CURLOPT_SSLKEYTYPE, s: optString.description) + case .sslVersion(let optVersion): + let value: Int + switch optVersion { + case .tlsV1: value = CURL_SSLVERSION_TLSv1 + case .tlsV1_1: value = CURL_SSLVERSION_TLSv1_1 + case .tlsV1_2: value = CURL_SSLVERSION_TLSv1_2 + } + curl.setOption(CURLOPT_SSLVERSION, int: value) + case .sslVerifyPeer(let optBool): + curl.setOption(CURLOPT_SSL_VERIFYPEER, int: optBool ? 1 : 0) + case .sslVerifyHost(let optBool): + curl.setOption(CURLOPT_SSL_VERIFYHOST, int: optBool ? 2 : 0) + case .sslCAFilePath(let optString): + curl.setOption(CURLOPT_CAINFO, s: optString) + case .sslCADirPath(let optString): + curl.setOption(CURLOPT_CAPATH, s: optString) + case .sslPinnedPublicKey(let optString): + curl.setOption(CURLOPT_PINNEDPUBLICKEY, s: optString) + case .sslCiphers(let optArray): + curl.setOption(CURLOPT_SSL_CIPHER_LIST, s: optArray.joined(separator: ":")) + case .ftpPreCommands(let optArray): + optArray.forEach { curl.setOption(CURLOPT_PREQUOTE, s: $0) } + case .ftpPostCommands(let optArray): + optArray.forEach { curl.setOption(CURLOPT_POSTQUOTE, s: $0) } + case .ftpPort(let optString): + curl.setOption(CURLOPT_FTPPORT, s: optString) + case .ftpResponseTimeout(let optInt): + curl.setOption(CURLOPT_FTP_RESPONSE_TIMEOUT, int: optInt) + case .sshPublicKey(let optString): + curl.setOption(CURLOPT_SSH_PUBLIC_KEYFILE, s: optString) + case .sshPrivateKey(let optString): + curl.setOption(CURLOPT_SSH_PRIVATE_KEYFILE, s: optString) + case .httpMethod(let optHTTPMethod): + switch optHTTPMethod { + case .get: curl.setOption(CURLOPT_HTTPGET, int: 1) + case .post: curl.setOption(CURLOPT_POST, int: 1) + case .head: curl.setOption(CURLOPT_NOBODY, int: 1) + case .patch: curl.setOption(CURLOPT_CUSTOMREQUEST, s: "PATCH") + case .delete, + .put, + .trace, + .options, + .connect, + .custom(_): curl.setOption(CURLOPT_CUSTOMREQUEST, s: optHTTPMethod.description) + } + case .postField(let optPOSTField): + if nil == request.postFields { + request.postFields = CURLRequest.POSTFields() + } + switch optPOSTField.type { + case .value: + _ = request.postFields?.append(key: optPOSTField.name, value: optPOSTField.value, mimeType: optPOSTField.mimeType ?? "") + case .file: + _ = request.postFields?.append(key: optPOSTField.name, path: optPOSTField.value, mimeType: optPOSTField.mimeType ?? "") + } + case .postData(let optBytes): + curl.setOption(CURLOPT_POSTFIELDSIZE_LARGE, int: optBytes.count) + curl.setOption(CURLOPT_COPYPOSTFIELDS, v: optBytes) + case .postString(let optString): + let bytes = Array(optString.utf8) + curl.setOption(CURLOPT_POSTFIELDSIZE_LARGE, int: bytes.count) + curl.setOption(CURLOPT_COPYPOSTFIELDS, v: bytes) + case .mailFrom(let optString): + curl.setOption(CURLOPT_MAIL_FROM, s: optString) + case .mailRcpt(let optString): + curl.setOption(CURLOPT_MAIL_RCPT, s: optString) + case .verbose: + curl.setOption(CURLOPT_VERBOSE, int: 1) + case .header: + curl.setOption(CURLOPT_HEADER, int: 1) + case .upload(let gen): + curl.setOption(CURLOPT_UPLOAD, int: 1) + request.uploadBodyGen = gen + if let len = gen.contentLength { + curl.setOption(CURLOPT_INFILESIZE_LARGE, int: len) + } + let opaqueRequest = Unmanaged.passRetained(request as AnyObject).toOpaque() + let curlFunc: curl_func = { + ptr, size, count, opaque -> Int in + guard let opaque = opaque, + let ptr = ptr else { + return 0 + } + let this = Unmanaged.fromOpaque(opaque).takeUnretainedValue() + guard let bytes = this.uploadBodyGen?.next(byteCount: size*count) else { + return 0 + } + memcpy(ptr, bytes, bytes.count) + return bytes.count + } + curl.setOption(CURLOPT_READDATA, v: opaqueRequest) + curl.setOption(CURLOPT_READFUNCTION, f: curlFunc) + case .acceptEncoding(let str): + curl.setOption(CURLOPT_ACCEPT_ENCODING, s: str) + } + } +} diff --git a/Sources/CurlyClient/CURLResponse.swift b/Sources/CurlyClient/CURLResponse.swift new file mode 100755 index 0000000..187a206 --- /dev/null +++ b/Sources/CurlyClient/CURLResponse.swift @@ -0,0 +1,305 @@ +// +// CURLResponse.swift +// PerfectCURL +// +// Created by Kyle Jessup on 2017-05-10. +// Copyright (C) 2017 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import CCurl +import Foundation + +enum ResponseReadState { + case status, headers, body +} + +/// Response for a CURLRequest. +/// Obtained by calling CURLResponse.perform. +open class CURLResponse { + /// A response header that can be retreived. + typealias Header = HTTPResponseHeader + /// A confirmation func thats used to obtain an asynchrnous response. + typealias Confirmation = () throws -> CURLResponse + /// An error thrown while retrieving a response. + struct Error: Swift.Error { + /// The curl specific request response code. + let code: Int + /// The string message for the curl response code. + let description: String + /// The response object for this error. + let response: CURLResponse + + init(_ response: CURLResponse, code: CURLcode) { + self.code = Int(code.rawValue) + self.description = response.curl.strError(code: code) + self.response = response + } + } + + /// Enum wrapping the typed response info keys. + enum Info { + /// Info keys with String values. + enum StringValue { + case + /// 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. + url, + /// The initial path that the request ended up at after logging in to the FTP server. + ftpEntryPath, + /// The URL that the request *would have* been redirected to. + redirectURL, + /// The local IP address that the request used most recently. + localIP, + /// The remote IP address that the request most recently connected to. + primaryIP, + /// The content type for the request. This is read from the "Content-Type" header. + contentType // TODO: this is provided directly by curl (for obvious reason) but might + // be confusing given that we parse headers and make them all available through `get` + } + /// Info keys with Int values. + enum IntValue { + case + /// The last received HTTP, FTP or SMTP response code. + responseCode, + /// The total size in bytes of all received headers. + headerSize, + /// 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. + requestSize, + /// The result of the SSL certificate verification. + sslVerifyResult, + // TODO: fileTime only works if the fileTime request option is set + fileTime, + /// The total number of redirections that were followed. + redirectCount, + /// The last received HTTP proxy response code to a CONNECT request. + httpConnectCode, + // TODO: this needs OptionSet enum + httpAuthAvail, + // TODO: this needs OptionSet enum + proxyAuthAvail, + /// The OS level errno which may have triggered a failure. + osErrno, + /// The number of connections that the request had to make in order to produce a response. + numConnects, + // TODO: requires the matching time condition options + conditionUnmet, + /// The remote port that the request most recently connected to + primaryPort, + /// The local port that the request used most recently + localPort +// httpVersion // not supported on ubuntu 16 curl?? + } + /// Info keys with Double values. + enum DoubleValue { + case + /// The total time in seconds for the previous request. + totalTime, + /// The total time in seconds from the start until the name resolving was completed. + nameLookupTime, + /// The total time in seconds from the start until the connection to the remote host or proxy was completed. + connectTime, + /// The time, in seconds, it took from the start until the file transfer is just about to begin. + preTransferTime, + /// The total number of bytes uploaded. + sizeUpload, // TODO: why is this a double? curl has it as a double + /// The total number of bytes downloaded. + sizeDownload, // TODO: why is this a double? curl has it as a double + /// The average download speed measured in bytes/second. + speedDownload, + /// The average upload speed measured in bytes/second. + speedUpload, + /// The content-length of the download. This value is obtained from the Content-Length header field. + contentLengthDownload, + /// The specified size of the upload. + contentLengthUpload, + /// The time, in seconds, it took from the start of the request until the first byte was received. + startTransferTime, + /// The total time, in seconds, it took for all redirection steps include name lookup, connect, pretransfer and transfer before final transaction was started. + redirectTime, + /// The time, in seconds, it took from the start until the SSL/SSH connect/handshake to the remote host was completed. + appConnectTime + } +// cookieList, // SLIST +// certInfo // SLIST + } + + let curl: CURL + internal(set) var headers = Array<(Header.Name, String)>() + + /// The response's raw content body bytes. + internal(set) var bodyBytes = [UInt8]() + + var readState = ResponseReadState.status + // these need to persist until the request has completed execution. + // this is set by the CURLRequest + var postFields: CURLRequest.POSTFields? + + init(_ curl: CURL, postFields: CURLRequest.POSTFields?) { + self.curl = curl + self.postFields = postFields + } +} + +extension CURLResponse { + /// Get an response info String value. + func get(_ stringValue: Info.StringValue) -> String? { + return stringValue.get(self) + } + /// Get an response info Int value. + func get(_ intValue: Info.IntValue) -> Int? { + return intValue.get(self) + } + /// Get an response info Double value. + func get(_ doubleValue: Info.DoubleValue) -> Double? { + return doubleValue.get(self) + } + /// Get a response header value. Returns the first found instance or nil. + func get(_ header: Header.Name) -> String? { + return headers.first { header.standardName == $0.0.standardName }?.1 + } + /// Get a response header's values. Returns all found instances. + func get(all header: Header.Name) -> [String] { + return headers.filter { header.standardName == $0.0.standardName }.map { $0.1 } + } +} + +extension CURLResponse { + func complete() throws { + setCURLOpts() + curl.addSLists() + let resultCode = curl_easy_perform(curl.curl) + postFields = nil + guard CURLE_OK == resultCode else { + throw Error(self, code: resultCode) + } + } + + func complete(_ callback: @escaping (Confirmation) -> ()) { + setCURLOpts() + innerComplete(callback) + } + + private func innerComplete(_ callback: @escaping (Confirmation) -> ()) { + let (notDone, resultCode, _, _) = curl.perform() + guard Int(CURLE_OK.rawValue) == resultCode else { + postFields = nil + return callback({ throw Error(self, code: CURLcode(rawValue: UInt32(resultCode))) }) + } + if notDone { + curl.ioWait { + self.innerComplete(callback) + } + } else { + postFields = nil + callback({ return self }) + } + } + + private func addHeaderLine(_ ptr: UnsafeBufferPointer) { + if readState == .status { + readState = .headers + } else if ptr.count == 0 { + readState = .body + } else { + let colon = 58 as UInt8, space = 32 as UInt8 + var pos = 0 + let max = ptr.count + + var tstNamePtr: UnsafeBufferPointer? + + while pos < max { + defer { pos += 1 } + if ptr[pos] == colon { + tstNamePtr = UnsafeBufferPointer(start: ptr.baseAddress, count: pos) + while pos < max && ptr[pos+1] == space { + pos += 1 + } + break + } + } + guard let namePtr = tstNamePtr, let base = ptr.baseAddress else { + return + } + let valueStart = base+pos + if valueStart[max-pos-1] == 10 { + pos += 1 + } + if valueStart[max-pos-1] == 13 { + pos += 1 + } + let valuePtr = UnsafeBufferPointer(start: valueStart, count: max-pos) + let name = String(bytes: namePtr, encoding: .utf8) ?? "" + let value = String(bytes: valuePtr, encoding: .utf8) ?? "" + headers.append((Header.Name.fromStandard(name: name), value)) + } + } + + private func addBodyData(_ ptr: UnsafeBufferPointer) { + bodyBytes.append(contentsOf: ptr) + } + + private func setCURLOpts() { + let opaqueMe = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + curl.setOption(CURLOPT_HEADERDATA, v: opaqueMe) + curl.setOption(CURLOPT_WRITEDATA, v: opaqueMe) + + do { + let readFunc: curl_func = { + a, size, num, p -> Int in + let crl = Unmanaged.fromOpaque(p!).takeUnretainedValue() + if let bytes = a?.assumingMemoryBound(to: UInt8.self) { + let fullCount = size*num + let minimumHeaderLengthEvenAMalformedOne = 3 + crl.addHeaderLine(UnsafeBufferPointer(start: bytes, + count: fullCount >= minimumHeaderLengthEvenAMalformedOne ? fullCount : 0)) + return fullCount + } + return 0 + } + curl.setOption(CURLOPT_HEADERFUNCTION, f: readFunc) + } + + do { + let readFunc: curl_func = { + a, size, num, p -> Int in + let crl = Unmanaged.fromOpaque(p!).takeUnretainedValue() + if let bytes = a?.assumingMemoryBound(to: UInt8.self) { + let fullCount = size*num + crl.addBodyData(UnsafeBufferPointer(start: bytes, count: fullCount)) + return fullCount + } + return 0 + } + curl.setOption(CURLOPT_WRITEFUNCTION, f: readFunc) + } + } +} + +extension CURLResponse { + /// Get the URL which the request may have been redirected to. + var url: String { return get(.url) ?? "" } + /// Get the HTTP response code + var responseCode: Int { return get(.responseCode) ?? 0 } +} + + + + + + + + + diff --git a/Sources/CurlyClient/CURLResponseInfos.swift b/Sources/CurlyClient/CURLResponseInfos.swift new file mode 100755 index 0000000..2ea0eb8 --- /dev/null +++ b/Sources/CurlyClient/CURLResponseInfos.swift @@ -0,0 +1,107 @@ +// +// CURLResponseInfos.swift +// PerfectCURL +// +// Created by Kyle Jessup on 2017-05-18. +// Copyright (C) 2017 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import CCurl + +protocol CURLResponseInfo { + associatedtype ValueType + func get(_ from: CURLResponse) -> ValueType? +} + +extension CURLResponse.Info.StringValue: CURLResponseInfo { + typealias ValueType = String + private var infoValue: CURLINFO { + switch self { + case .url: return CURLINFO_EFFECTIVE_URL + case .ftpEntryPath: return CURLINFO_FTP_ENTRY_PATH + case .redirectURL: return CURLINFO_REDIRECT_URL + case .localIP: return CURLINFO_LOCAL_IP + case .primaryIP: return CURLINFO_PRIMARY_IP + case .contentType: return CURLINFO_CONTENT_TYPE + } + } + + func get(_ from: CURLResponse) -> String? { + let (i, code): (String, CURLcode) = from.curl.getInfo(infoValue) + guard code == CURLE_OK else { + return nil + } + return i + } +} + +extension CURLResponse.Info.IntValue: CURLResponseInfo { + typealias ValueType = Int + private var infoValue: CURLINFO { + switch self { + case .responseCode: return CURLINFO_RESPONSE_CODE + case .headerSize: return CURLINFO_HEADER_SIZE + case .requestSize: return CURLINFO_REQUEST_SIZE + case .sslVerifyResult: return CURLINFO_SSL_VERIFYRESULT + case .fileTime: return CURLINFO_FILETIME + case .redirectCount: return CURLINFO_REDIRECT_COUNT + case .httpConnectCode: return CURLINFO_HTTP_CONNECTCODE + case .httpAuthAvail: return CURLINFO_HTTPAUTH_AVAIL + case .proxyAuthAvail: return CURLINFO_PROXYAUTH_AVAIL + case .osErrno: return CURLINFO_OS_ERRNO + case .numConnects: return CURLINFO_NUM_CONNECTS + case .conditionUnmet: return CURLINFO_CONDITION_UNMET + case .primaryPort: return CURLINFO_PRIMARY_PORT + case .localPort: return CURLINFO_LOCAL_PORT +// case .httpVersion: return CURLINFO_HTTP_VERSION + } + } + + func get(_ from: CURLResponse) -> Int? { + let (i, code): (Int, CURLcode) = from.curl.getInfo(infoValue) + guard code == CURLE_OK else { + return nil + } + return i + } +} + +extension CURLResponse.Info.DoubleValue: CURLResponseInfo { + typealias ValueType = Double + private var infoValue: CURLINFO { + switch self { + case .totalTime: return CURLINFO_TOTAL_TIME + case .nameLookupTime: return CURLINFO_NAMELOOKUP_TIME + case .connectTime: return CURLINFO_CONNECT_TIME + case .preTransferTime: return CURLINFO_PRETRANSFER_TIME + case .sizeUpload: return CURLINFO_SIZE_UPLOAD + case .sizeDownload: return CURLINFO_SIZE_DOWNLOAD + case .speedDownload: return CURLINFO_SPEED_DOWNLOAD + case .speedUpload: return CURLINFO_SPEED_UPLOAD + case .contentLengthDownload: return CURLINFO_CONTENT_LENGTH_DOWNLOAD + case .contentLengthUpload: return CURLINFO_CONTENT_LENGTH_UPLOAD + case .startTransferTime: return CURLINFO_STARTTRANSFER_TIME + case .redirectTime: return CURLINFO_REDIRECT_TIME + case .appConnectTime: return CURLINFO_APPCONNECT_TIME + } + } + + func get(_ from: CURLResponse) -> Double? { + let (d, code): (Double, CURLcode) = from.curl.getInfo(infoValue) + guard code == CURLE_OK else { + return nil + } + return d + } +} diff --git a/Sources/CurlyClient/CurlyClient.swift b/Sources/CurlyClient/CurlyClient.swift index 018f806..6e36713 100644 --- a/Sources/CurlyClient/CurlyClient.swift +++ b/Sources/CurlyClient/CurlyClient.swift @@ -1,5 +1,4 @@ import Vapor -import PerfectCURL public final class CurlyClient: Client, ServiceType { public var container: Container diff --git a/Sources/CurlyClient/HTTPHeaders.swift b/Sources/CurlyClient/HTTPHeaders.swift new file mode 100755 index 0000000..e14216a --- /dev/null +++ b/Sources/CurlyClient/HTTPHeaders.swift @@ -0,0 +1,370 @@ +// +// HTTPHeaders.swift +// PerfectLib +// +// Created by Kyle Jessup on 2016-06-17. +// Copyright (C) 2016 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +/// An HTTP request header. +enum HTTPRequestHeader { + /// A header name type. Each has a corresponding value type. + enum Name: Hashable { + case accept, acceptCharset, acceptEncoding, acceptLanguage, acceptDatetime, authorization + case cacheControl, connection, cookie, contentLength, contentMD5, contentType + case date, expect, forwarded, from, host + case ifMatch, ifModifiedSince, ifNoneMatch, ifRange, ifUnmodifiedSince + case maxForwards, origin, pragma, proxyAuthorization, range, referer + case te, userAgent, upgrade, via, warning, xRequestedWith, xRequestedBy, dnt + case xAuthorization, xForwardedFor, xForwardedHost, xForwardedProto + case frontEndHttps, xHttpMethodOverride, xATTDeviceId, xWapProfile + case proxyConnection, xUIDH, xCsrfToken, accessControlRequestMethod, accessControlRequestHeaders + case xB3TraceId, xB3SpanId, xB3ParentSpanId + case custom(name: String) + + var hashValue: Int { + return self.standardName.lowercased().hashValue + } + + var standardName: String { + switch self { + case .accept: return "Accept" + case .acceptCharset: return "Accept-Charset" + case .acceptEncoding: return "Accept-Encoding" + case .acceptLanguage: return "Accept-Language" + case .acceptDatetime: return "Accept-Datetime" + case .accessControlRequestMethod: return "Access-Control-Request-Method" + case .accessControlRequestHeaders: return "Access-Control-Request-Headers" + case .authorization: return "Authorization" + case .cacheControl: return "Cache-Control" + case .connection: return "Connection" + case .cookie: return "Cookie" + case .contentLength: return "Content-Length" + case .contentMD5: return "Content-MD5" + case .contentType: return "Content-Type" + case .date: return "Date" + case .expect: return "Expect" + case .forwarded: return "Forwarded" + case .from: return "From" + case .host: return "Host" + case .ifMatch: return "If-Match" + case .ifModifiedSince: return "If-Modified-Since" + case .ifNoneMatch: return "If-None-Match" + case .ifRange: return "If-Range" + case .ifUnmodifiedSince: return "If-Unmodified-Since" + case .maxForwards: return "Max-Forwards" + case .origin: return "Origin" + case .pragma: return "Pragma" + case .proxyAuthorization: return "Proxy-Authorization" + case .range: return "Range" + case .referer: return "Referer" + case .te: return "TE" + case .userAgent: return "User-Agent" + case .upgrade: return "Upgrade" + case .via: return "Via" + case .warning: return "Warning" + case .xAuthorization: return "X-Authorization" + case .xRequestedWith: return "X-Requested-with" + case .xRequestedBy: return "X-Requested-by" + case .dnt: return "DNT" + case .xForwardedFor: return "X-Forwarded-For" + case .xForwardedHost: return "X-Forwarded-Host" + case .xForwardedProto: return "X-Forwarded-Proto" + case .frontEndHttps: return "Front-End-Https" + case .xHttpMethodOverride: return "X-HTTP-Method-Override" + case .xATTDeviceId: return "X-Att-Deviceid" + case .xWapProfile: return "X-WAP-Profile" + case .proxyConnection: return "Proxy-Connection" + case .xUIDH: return "X-UIDH" + case .xCsrfToken: return "X-CSRF-Token" + case .xB3TraceId: return "X-B3-TraceId" + case .xB3SpanId: return "X-B3-SpanId" + case .xB3ParentSpanId: return "X-B3-ParentSpanId" + case .custom(let str): return str + } + } + + static let lookupTable: [String:HTTPRequestHeader.Name] = [ + "accept":.accept, + "accept-charset":.acceptCharset, + "accept-encoding":.acceptEncoding, + "accept-language":.acceptLanguage, + "accept-datetime":.acceptDatetime, + "access-control-request-method":.accessControlRequestMethod, + "access-control-request-headers":.accessControlRequestHeaders, + "authorization":.authorization, + "cache-control":.cacheControl, + "connection":.connection, + "cookie":.cookie, + "content-length":.contentLength, + "content-md5":.contentMD5, + "content-type":.contentType, + "date":.date, + "expect":.expect, + "forwarded":.forwarded, + "from":.from, + "host":.host, + "if-match":.ifMatch, + "if-modified-since":.ifModifiedSince, + "if-none-match":.ifNoneMatch, + "if-range":.ifRange, + "if-unmodified-since":.ifUnmodifiedSince, + "max-forwards":.maxForwards, + "origin":.origin, + "pragma":.pragma, + "proxy-authorization":.proxyAuthorization, + "range":.range, + "referer":.referer, + "te":.te, + "user-agent":.userAgent, + "upgrade":.upgrade, + "via":.via, + "warning":.warning, + "x-requested-with":.xRequestedWith, + "x-requested-by":.xRequestedBy, + "dnt":.dnt, + "x-authorization":.xAuthorization, + "x-forwarded-for":.xForwardedFor, + "x-forwarded-host":.xForwardedHost, + "x-forwarded-proto":.xForwardedProto, + "front-end-https":.frontEndHttps, + "x-http-method-override":.xHttpMethodOverride, + "x-att-deviceid":.xATTDeviceId, + "x-wap-profile":.xWapProfile, + "proxy-connection":.proxyConnection, + "x-uidh":.xUIDH, + "x-csrf-token":.xCsrfToken, + "x-b3-traceid":.xB3TraceId, + "x-b3-spanid":.xB3SpanId, + "x-b3-parentspanid":.xB3ParentSpanId + ] + + static func fromStandard(name: String) -> HTTPRequestHeader.Name { + if let found = HTTPRequestHeader.Name.lookupTable[name.lowercased()] { + return found + } + return .custom(name: name) + } + } +} + +func ==(lhs: HTTPRequestHeader.Name, rhs: HTTPRequestHeader.Name) -> Bool { + return lhs.standardName.lowercased() == rhs.standardName.lowercased() +} + +/// A HTTP response header. +enum HTTPResponseHeader { + + enum Name { + case accessControlAllowOrigin + case accessControlAllowMethods + case accessControlAllowCredentials + case accessControlAllowHeaders + case accessControlMaxAge + case acceptPatch + case acceptRanges + case age + case allow + case altSvc + case cacheControl + case connection + case contentDisposition + case contentEncoding + case contentLanguage + case contentLength + case contentLocation + case contentMD5 + case contentRange + case contentType + case date + case eTag + case expires + case lastModified + case link + case location + case p3p + case pragma + case proxyAuthenticate + case publicKeyPins + case refresh + case retryAfter + case server + case setCookie + case status + case strictTransportSecurity + case trailer + case transferEncoding + case tsv + case upgrade + case vary + case via + case warning + case wwwAuthenticate + case xFrameOptions + case xxsSProtection + case contentSecurityPolicy + case xContentSecurityPolicy + case xWebKitCSP + case xContentTypeOptions + case xPoweredBy + case xUACompatible + case xContentDuration + case upgradeInsecureRequests + case xRequestID + case xCorrelationID + case xB3TraceId + case xB3SpanId + case xB3ParentSpanId + case custom(name: String) + + var hashValue: Int { + return self.standardName.lowercased().hashValue + } + + var standardName: String { + switch self { + case .accessControlAllowOrigin: return "Access-Control-Allow-Origin" + case .accessControlAllowMethods: return "Access-Control-Allow-Methods" + case .accessControlAllowCredentials: return "Access-Control-Allow-Credentials" + case .accessControlAllowHeaders: return "Access-Control-Allow-Headers" + case .accessControlMaxAge: return "Access-Control-Max-Age" + case .acceptPatch: return "Accept-Patch" + case .acceptRanges: return "Accept-Ranges" + case .age: return "Age" + case .allow: return "Allow" + case .altSvc: return "Alt-Svc" + case .cacheControl: return "Cache-Control" + case .connection: return "Connection" + case .contentDisposition: return "Content-Disposition" + case .contentEncoding: return "Content-Encoding" + case .contentLanguage: return "Content-Language" + case .contentLength: return "Content-Length" + case .contentLocation: return "Content-Location" + case .contentMD5: return "Content-MD5" + case .contentRange: return "Content-Range" + case .contentType: return "Content-Type" + case .date: return "Date" + case .eTag: return "ETag" + case .expires: return "Expires" + case .lastModified: return "Last-Modified" + case .link: return "Link" + case .location: return "Location" + case .p3p: return "P3P" + case .pragma: return "Pragma" + case .proxyAuthenticate: return "Proxy-Authenticate" + case .publicKeyPins: return "Public-Key-Pins" + case .refresh: return "Refresh" + case .retryAfter: return "Retry-After" + case .server: return "Server" + case .setCookie: return "Set-Cookie" + case .status: return "Status" + case .strictTransportSecurity: return "Strict-Transport-Security" + case .trailer: return "Trailer" + case .transferEncoding: return "Transfer-Encoding" + case .tsv: return "TSV" + case .upgrade: return "Upgrade" + case .vary: return "Vary" + case .via: return "Via" + case .warning: return "Warning" + case .wwwAuthenticate: return "WWW-Authenticate" + case .xFrameOptions: return "X-Frame-Options" + case .xxsSProtection: return "X-XSS-Protection" + case .contentSecurityPolicy: return "Content-Security-Policy" + case .xContentSecurityPolicy: return "X-Content-Security-Policy" + case .xWebKitCSP: return "X-WebKit-CSP" + case .xContentTypeOptions: return "X-Content-Type-Options" + case .xPoweredBy: return "X-Powered-By" + case .xUACompatible: return "X-UA-Compatible" + case .xContentDuration: return "X-Content-Duration" + case .upgradeInsecureRequests: return "Upgrade-Insecure-Requests" + case .xRequestID: return "X-Request-ID" + case .xCorrelationID: return "X-Correlation-ID" + case .xB3TraceId: return "X-B3-TraceId" + case .xB3SpanId: return "X-B3-SpanId" + case .xB3ParentSpanId: return "X-B3-ParentSpanId" + case .custom(let str): return str + } + } + + static func fromStandard(name: String) -> HTTPResponseHeader.Name { + switch name.lowercased() { + case "access-control-Allow-Origin": return .accessControlAllowOrigin + case "access-control-Allow-Methods": return .accessControlAllowMethods + case "access-control-Allow-Credentials": return .accessControlAllowCredentials + case "access-control-Allow-Headers": return .accessControlAllowHeaders + case "access-control-Max-Age": return .accessControlMaxAge + case "accept-patch": return .acceptPatch + case "accept-ranges": return .acceptRanges + case "age": return .age + case "allow": return .allow + case "alt-svc": return .altSvc + case "cache-control": return .cacheControl + case "connection": return .connection + case "content-disposition": return .contentDisposition + case "content-encoding": return .contentEncoding + case "content-language": return .contentLanguage + case "content-length": return .contentLength + case "content-location": return .contentLocation + case "content-mD5": return .contentMD5 + case "content-range": return .contentRange + case "content-type": return .contentType + case "date": return .date + case "etag": return .eTag + case "expires": return .expires + case "last-modified": return .lastModified + case "link": return .link + case "location": return .location + case "p3p": return .p3p + case "pragma": return .pragma + case "proxy-authenticate": return .proxyAuthenticate + case "public-key-pins": return .publicKeyPins + case "refresh": return .refresh + case "retry-after": return .retryAfter + case "server": return .server + case "set-cookie": return .setCookie + case "status": return .status + case "strict-transport-security": return .strictTransportSecurity + case "srailer": return .trailer + case "sransfer-encoding": return .transferEncoding + case "ssv": return .tsv + case "upgrade": return .upgrade + case "vary": return .vary + case "via": return .via + case "warning": return .warning + case "www-authenticate": return .wwwAuthenticate + case "x-frame-options": return .xFrameOptions + case "x-xss-protection": return .xxsSProtection + case "content-security-policy": return .contentSecurityPolicy + case "x-content-security-policy": return .xContentSecurityPolicy + case "x-webkit-csp": return .xWebKitCSP + case "x-content-type-options": return .xContentTypeOptions + case "x-powered-by": return .xPoweredBy + case "x-ua-compatible": return .xUACompatible + case "x-content-duration": return .xContentDuration + case "upgrade-insecure-requests": return .upgradeInsecureRequests + case "x-request-id": return .xRequestID + case "x-correlation-id": return .xCorrelationID + case "x-b3-traceid": return .xB3TraceId + case "x-b3-spanid": return .xB3SpanId + case "x-b3-parentspanid": return .xB3ParentSpanId + + default: return .custom(name: name) + } + } + } +} + +func ==(lhs: HTTPResponseHeader.Name, rhs: HTTPResponseHeader.Name) -> Bool { + return lhs.standardName.lowercased() == rhs.standardName.lowercased() +} diff --git a/Sources/CurlyClient/HTTPMethod.swift b/Sources/CurlyClient/HTTPMethod.swift new file mode 100755 index 0000000..f6eafa3 --- /dev/null +++ b/Sources/CurlyClient/HTTPMethod.swift @@ -0,0 +1,87 @@ +// +// HTTPMethod.swift +// PerfectLib +// +// Created by Kyle Jessup on 2016-06-20. +// Copyright (C) 2016 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +/// HTTP request method types +enum HTTPMethod: Hashable, CustomStringConvertible { + /// OPTIONS + case options, + /// GET + get, + /// HEAD + head, + /// POST + post, + /// PATCH + patch, + /// PUT + put, + /// DELETE + delete, + /// TRACE + trace, + /// CONNECT + connect, + /// Any unaccounted for or custom method + custom(String) + /// All non-custom methods + static var allMethods: [HTTPMethod] { + return [.options, .get, .head, .post, .patch, .put, .delete, .trace, .connect] + } + + static func from(string: String) -> HTTPMethod { + switch string { + case "OPTIONS": return .options + case "GET": return .get + case "HEAD": return .head + case "POST": return .post + case "PATCH": return .patch + case "PUT": return .put + case "DELETE": return .delete + case "TRACE": return .trace + case "CONNECT": return .connect + default: return .custom(string) + } + } + + /// Method String hash value + var hashValue: Int { + return self.description.hashValue + } + + /// The method as a String + var description: String { + switch self { + case .options: return "OPTIONS" + case .get: return "GET" + case .head: return "HEAD" + case .post: return "POST" + case .patch: return "PATCH" + case .put: return "PUT" + case .delete: return "DELETE" + case .trace: return "TRACE" + case .connect: return "CONNECT" + case .custom(let s): return s + } + } +} + +/// Compare two HTTP methods +func == (lhs: HTTPMethod, rhs: HTTPMethod) -> Bool { + return lhs.description == rhs.description +} diff --git a/Sources/CurlyClient/cURL.swift b/Sources/CurlyClient/cURL.swift new file mode 100755 index 0000000..ffa6bb6 --- /dev/null +++ b/Sources/CurlyClient/cURL.swift @@ -0,0 +1,505 @@ +// +// cURL.swift +// PerfectLib +// +// Created by Kyle Jessup on 2015-08-10. +// Copyright (C) 2015 PerfectlySoft, Inc. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import CCurl +import Dispatch + +/// This class is a wrapper around the CURL library. It permits network operations to be completed using cURL in a block or non-blocking manner. +public class CURL { + + static var sInit: Int = { + curl_global_init(Int(CURL_GLOBAL_SSL | CURL_GLOBAL_WIN32)) + return 1 + }() + + var curl: UnsafeMutableRawPointer? + var multi: UnsafeMutableRawPointer? + + typealias SList = UnsafeMutablePointer + + var slistMap = [UInt32:SList]() + + var headerBytes = [UInt8]() + var bodyBytes = [UInt8]() + + /// The CURLINFO_RESPONSE_CODE for the last operation. + public var responseCode: Int { + return self.getInfo(CURLINFO_RESPONSE_CODE).0 + } + + /// Get or set the current URL. + public var url: String { + get { + return self.getInfo(CURLINFO_EFFECTIVE_URL).0 + } + set { + let _ = self.setOption(CURLOPT_URL, s: newValue) + } + } + + /// Initialize the CURL request. + public init() { + _ = CURL.sInit + self.curl = curl_easy_init() + setCurlOpts() + } + + /// Initialize the CURL request with a given URL. + public convenience init(url: String) { + self.init() + self.url = url + } + + /// Duplicate the given request into a new CURL object. + public init(dupeCurl: CURL) { + if let copyFrom = dupeCurl.curl { + self.curl = curl_easy_duphandle(copyFrom) + } else { + self.curl = curl_easy_init() + } + setCurlOpts() // still set options + } + + func setCurlOpts() { + guard let curl = self.curl else { + return + } + curl_easy_setopt_long(curl, CURLOPT_NOSIGNAL, 1) + let opaqueMe = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + let _ = setOption(CURLOPT_HEADERDATA, v: opaqueMe) + let _ = setOption(CURLOPT_WRITEDATA, v: opaqueMe) + let _ = setOption(CURLOPT_READDATA, v: opaqueMe) + + let headerReadFunc: curl_func = { + (a, size, num, p) -> Int in + + let crl = Unmanaged.fromOpaque(p!).takeUnretainedValue() + if let bytes = a?.assumingMemoryBound(to: UInt8.self) { + let fullCount = size*num + for idx in 0.. Int in + + let crl = Unmanaged.fromOpaque(p!).takeUnretainedValue() + if let bytes = a?.assumingMemoryBound(to: UInt8.self) { + let fullCount = size*num + for idx in 0...self)) + return 0 + }) + + } + + private func clearSListMap() { + slistMap.forEach { + _, ptr in + curl_slist_free_all(ptr) + } + slistMap = [:] + } + + /// Clean up and reset the CURL object for further use. + /// Sets default options such as header/body read callbacks. + public func reset() { + guard let curl = self.curl else { + return + } + if let multi = self.multi { + curl_multi_remove_handle(multi, curl) + curl_multi_cleanup(multi) + self.multi = nil + } + curl_easy_reset(curl) + clearSListMap() + setCurlOpts() + } + + /// Cleanup and close the CURL request. Object should not be used again. + /// This is called automatically when the object goes out of scope. + /// It is safe to call this multiple times. + public func close() { + guard let curl = self.curl else { + return + } + if let multi = self.multi { + curl_multi_remove_handle(multi, curl) + curl_multi_cleanup(multi) + self.multi = nil + } + curl_easy_cleanup(curl) + clearSListMap() + self.curl = nil + } + + deinit { + self.close() + } + + private class InternalResponseAccumulator { + var header = [UInt8]() + var body = [UInt8]() + } + + func addSLists() { + slistMap.forEach { + key, value in + curl_easy_setopt_slist(curl, CURLoption(rawValue: key), value) + } + } + + /// Perform the CURL request in a non-blocking manner. The closure will be called with the resulting code, header and body data. + public func perform(closure: @escaping (Int, [UInt8], [UInt8]) -> ()) { + guard let curl = self.curl else { + return closure(-1, [UInt8](), [UInt8]()) + } + addSLists() + let accum = InternalResponseAccumulator() + if nil == self.multi { + self.multi = curl_multi_init() + } + curl_multi_add_handle(multi, curl) + performInner(accumulator: accum, closure: closure) + } + + private func performInner(accumulator: InternalResponseAccumulator, closure: @escaping (Int, [UInt8], [UInt8]) -> ()) { + let perf = self.perform() + if let h = perf.2 { + _ = accumulator.header.append(contentsOf: h) + } + if let b = perf.3 { + _ = accumulator.body.append(contentsOf: b) + } + if perf.0 == false { // done + closure(perf.1, accumulator.header, accumulator.body) + } else { + ioWait { + self.performInner(accumulator: accumulator, closure: closure) + } + } + } + + func ioWait(_ closure: @escaping () -> ()) { + var timeout = 0 + curl_multi_timeout(self.multi, &timeout) + if timeout == 0 { + return closure() + } + + var fdsRd = fd_set(), fdsWr = fd_set(), fdsEx = fd_set() + var fdsZero = fd_set() + memset(&fdsZero, 0, MemoryLayout.size) + memset(&fdsRd, 0, MemoryLayout.size) + memset(&fdsWr, 0, MemoryLayout.size) + memset(&fdsEx, 0, MemoryLayout.size) + var max = Int32(0) + curl_multi_fdset(self.multi, &fdsRd, &fdsWr, &fdsEx, &max) + + var tv = timeval() + tv.tv_sec = timeout/1000 + #if os(Linux) + tv.tv_usec = Int((timeout%1000)*1000) + #else + tv.tv_usec = Int32((timeout%1000)*1000) + #endif + if max == -1 { + DispatchQueue.global().async { + closure() + } + } else { + // wait for write + DispatchQueue.global().async { + select(max+1, &fdsRd, &fdsWr, &fdsEx, &tv) + closure() + } + } + } + + /// Performs the request, blocking the current thread until it completes. + /// - returns: A tuple consisting of: Int - the result code, [UInt8] - the header bytes if any, [UInt8] - the body bytes if any + public func performFully() -> (Int, [UInt8], [UInt8]) { + // revisit this deprecation for a minor point release @available(*, deprecated, message: "Use performFullySync() instead") + guard let curl = self.curl else { + return (-1, [UInt8](), [UInt8]()) + } + addSLists() + let code = curl_easy_perform(curl) + defer { + self.headerBytes = [UInt8]() + self.bodyBytes = [UInt8]() + self.reset() + } + if code != CURLE_OK { + let str = self.strError(code: code) + print(str) + } + return (Int(code.rawValue), self.headerBytes, self.bodyBytes) + } + + /// Performs the request, blocking the current thread until it completes. + /// - returns: A tuple consisting of: Int - the result code, Int - the response code, [UInt8] - the header bytes if any, [UInt8] - the body bytes if any + public func performFullySync() -> (resultCode: Int, responseCode: Int, headerBytes: [UInt8], bodyBytes: [UInt8]) { + guard let curl = self.curl else { + return (-1, -1, [UInt8](), [UInt8]()) + } + addSLists() + let code = curl_easy_perform(curl) + defer { + self.headerBytes = [UInt8]() + self.bodyBytes = [UInt8]() + self.reset() + } + if code != CURLE_OK { + let str = self.strError(code: code) + print(str) + } + return (Int(code.rawValue), self.responseCode, self.headerBytes, self.bodyBytes) + } + + /// Performs a bit of work on the current request. + /// - returns: A tuple consisting of: Bool - should perform() be called again, Int - the result code, [UInt8] - the header bytes if any, [UInt8] - the body bytes if any + public func perform() -> (Bool, Int, [UInt8]?, [UInt8]?) { + guard let curl = self.curl else { + return (false, -1, nil, nil) + } + if self.multi == nil { + addSLists() + let multi = curl_multi_init() + self.multi = multi + curl_multi_add_handle(multi, curl) + } + guard let multi = self.multi else { + return (false, -1, nil, nil) + } + var one: Int32 = 0 + var code = CURLM_OK + repeat { + + code = curl_multi_perform(multi, &one) + + } while code == CURLM_CALL_MULTI_PERFORM + + guard code == CURLM_OK else { + return (false, Int(code.rawValue), nil, nil) + } + var two: Int32 = 0 + let msg = curl_multi_info_read(multi, &two) + + defer { + if self.headerBytes.count > 0 { + self.headerBytes = [UInt8]() + } + if self.bodyBytes.count > 0 { + self.bodyBytes = [UInt8]() + } + } + + if msg != nil { + let msgResult = curl_get_msg_result(msg) + guard msgResult == CURLE_OK else { + return (false, Int(msgResult.rawValue), nil, nil) + } + return (false, Int(msgResult.rawValue), + self.headerBytes.count > 0 ? self.headerBytes : nil, + self.bodyBytes.count > 0 ? self.bodyBytes : nil) + } + return (true, 0, + self.headerBytes.count > 0 ? self.headerBytes : nil, + self.bodyBytes.count > 0 ? self.bodyBytes : nil) + } + + /// Returns the String message for the given CURL result code. + public func strError(code cod: CURLcode) -> String { + return String(validatingUTF8: curl_easy_strerror(cod))! + } + + /// Returns the Int value for the given CURLINFO. + public func getInfo(_ info: CURLINFO) -> (Int, CURLcode) { + guard let curl = self.curl else { + return (-1, CURLE_FAILED_INIT) + } + var i = 0 + let c = curl_easy_getinfo_long(curl, info, &i) + return (i, c) + } + + /// Returns the Double value for the given CURLINFO. + public func getInfo(_ info: CURLINFO) -> (Double, CURLcode) { + guard let curl = self.curl else { + return (-1, CURLE_FAILED_INIT) + } + var d = 0.0 + let c = curl_easy_getinfo_double(curl, info, &d) + return (d, c) + } + + /// Returns the String value for the given CURLINFO. + public func getInfo(_ info: CURLINFO) -> (String, CURLcode) { + guard let curl = self.curl else { + return ("Not initialized", CURLE_FAILED_INIT) + } + var i: UnsafePointer? = nil + let code = curl_easy_getinfo_cstr(curl, info, &i) + guard code == CURLE_OK, let p = i, let str = String(validatingUTF8: p) else { + return ("", code) + } + return (str, code) + } + + /// Sets the Int64 option value. + @discardableResult + public func setOption(_ option: CURLoption, int: Int64) -> CURLcode { + guard let curl = self.curl else { + return CURLE_FAILED_INIT + } + return curl_easy_setopt_int64(curl, option, int) + } + + /// Sets the Int option value. + @discardableResult + public func setOption(_ option: CURLoption, int: Int) -> CURLcode { + guard let curl = self.curl else { + return CURLE_FAILED_INIT + } + return curl_easy_setopt_long(curl, option, int) + } + + /// Sets the pointer option value. + /// Note that the pointer value is not copied or otherwise manipulated or saved. + /// It is up to the caller to ensure the pointer value has a lifetime which corresponds to its usage. + @discardableResult + public func setOption(_ option: CURLoption, v: UnsafeRawPointer) -> CURLcode { + guard let curl = self.curl else { + return CURLE_FAILED_INIT + } + let nv = UnsafeMutableRawPointer(mutating: v) + return curl_easy_setopt_void(curl, option, nv) + } + + /// Sets the callback function option value. + @discardableResult + public func setOption(_ option: CURLoption, f: @escaping curl_func) -> CURLcode { + guard let curl = self.curl else { + return CURLE_FAILED_INIT + } + return curl_easy_setopt_func(curl, option, f) + } + + private func appendSList(key: UInt32, value: String) { + let old = slistMap[key] + let new = curl_slist_append(old, value) + slistMap[key] = new + } + + /// Sets the String option value. + @discardableResult + public func setOption(_ option: CURLoption, s: String) -> CURLcode { + guard let curl = self.curl else { + return CURLE_FAILED_INIT + } + switch(option.rawValue) { + case CURLOPT_HTTP200ALIASES.rawValue, + CURLOPT_HTTPHEADER.rawValue, + CURLOPT_POSTQUOTE.rawValue, + CURLOPT_PREQUOTE.rawValue, + CURLOPT_QUOTE.rawValue, + //CURLOPT_MAIL_FROM.rawValue, + CURLOPT_MAIL_RCPT.rawValue: + appendSList(key: option.rawValue, value: s) + return CURLE_OK + default: + () + } + return curl_easy_setopt_cstr(curl, option, s) + } + + public class POSTFields { + var first = UnsafeMutablePointer(bitPattern: 0) + var last = UnsafeMutablePointer(bitPattern: 0) + + /// constructor, create a blank form without any fields + /// must append each field manually + public init() { } + + /// add a post field + /// - parameters: + /// - key: post field name + /// - value: post field value string + /// - type: post field type, e.g., "text/html". + /// - returns: + /// CURLFORMCode, 0 for ok + public func append(key: String, value: String, mimeType: String = "") -> CURLFORMcode { + return curl_formadd_content(&first, &last, key, value, 0, mimeType.isEmpty ? nil : mimeType) + }//end append + + /// add a post field + /// - parameters: + /// - key: post field name + /// - buffer: post field value, binary buffer + /// - type: post field type, e.g., "image/jpeg". + /// - throws: + /// CURLFORMCode, 0 for ok + public func append(key: String, buffer: [Int8], mimeType: String = "") -> CURLFORMcode { + return curl_formadd_content(&first, &last, key, buffer, buffer.count, mimeType.isEmpty ? nil : mimeType) + }//end append + + /// add a post field + /// - parameters: + /// - key: post field name + /// - value: post field value string + /// - type: post field mime type, e.g., "image/jpeg". + /// - throws: + /// CURLFORMCode, 0 for ok + public func append(key: String, path: String, mimeType: String = "") -> CURLFORMcode { + return curl_formadd_file(&first, &last, key, path, mimeType.isEmpty ? nil : mimeType) + }//end append + + deinit { + curl_formfree(first) + //curl_formfree(last) + }//end deinit + }//end class + + /// Post a form with different fields. + @discardableResult + public func formAddPost(fields: POSTFields) -> CURLcode { + guard let p = fields.first else { + return CURLcode(rawValue: 4096) + }//end guard + return curl_form_post(self.curl, p) + }//end formAddPost +}