diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..f398a20 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +3.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2320f3a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# Change Log +All notable changes to this project will be documented in this file. +`SwiftHTTP` adheres to [Semantic Versioning](http://semver.org/). + +#### [3.0.1](https://github.com/daltoniam/SwiftHTTP/tree/3.0.1) + +fixed: +[#282](https://github.com/daltoniam/SwiftHTTP/pull/282) +[#281](https://github.com/daltoniam/SwiftHTTP/pull/281) +[#280](https://github.com/daltoniam/SwiftHTTP/issues/280) + + +Small bug fixes. + +#### [3.0.0](https://github.com/daltoniam/SwiftHTTP/tree/3.0.0) + +Refactor that has a simpler design. Single framework (no more platform suffixes! e.g. SwiftHTTPOSX, SwiftHTTPTVOS, etc). + +fixed: +[#278](https://github.com/daltoniam/SwiftHTTP/issues/278) +[#272](https://github.com/daltoniam/SwiftHTTP/pull/272) +[#251](https://github.com/daltoniam/SwiftHTTP/issues/251) +[#226](https://github.com/daltoniam/SwiftHTTP/issues/226) +[#140](https://github.com/daltoniam/SwiftHTTP/issues/140) + +#### [2.1.0](https://github.com/daltoniam/SwiftHTTP/tree/2.1.0) + +Update for Swift 4 support + +fixed: +[#259](https://github.com/daltoniam/SwiftHTTP/issues/259) +[#265](https://github.com/daltoniam/SwiftHTTP/issues/265) + +#### [2.0.2](https://github.com/daltoniam/SwiftHTTP/tree/2.0.2) + +Small fix for JSON serializer. + +#### [2.0.1](https://github.com/daltoniam/SwiftHTTP/tree/2.0.1) + +Addresses bugs from the Swift 3 port. + +fixed: +[#249](https://github.com/daltoniam/SwiftHTTP/issues/249) +[#248](https://github.com/daltoniam/SwiftHTTP/issues/248) + +#### [2.0.0](https://github.com/daltoniam/SwiftHTTP/tree/2.0.0) + +Swift 3.0 update. + +fixed: +[#192](https://github.com/daltoniam/SwiftHTTP/issues/192) +[#240](https://github.com/daltoniam/SwiftHTTP/issues/240) +[#241](https://github.com/daltoniam/SwiftHTTP/issues/241) +[#245](https://github.com/daltoniam/SwiftHTTP/issues/245) + +#### [1.0.5](https://github.com/daltoniam/SwiftHTTP/tree/1.0.5) + +Swift 2.2 update. + +#### [1.0.4](https://github.com/daltoniam/SwiftHTTP/tree/1.0.4) + +fixed: +[#215](https://github.com/daltoniam/SwiftHTTP/issues/215) + +#### [1.0.3](https://github.com/daltoniam/SwiftHTTP/tree/1.0.3) + +Changed: +Add Swift package manager support +TVOS support +WatchOS support + +fixed: +[#195](https://github.com/daltoniam/SwiftHTTP/issues/195) +[#203](https://github.com/daltoniam/SwiftHTTP/issues/203) + +#### [1.0.2](https://github.com/daltoniam/SwiftHTTP/tree/1.0.2) + +Parameter encoding fixes ([#178](https://github.com/daltoniam/SwiftHTTP/issues/178), [#182](https://github.com/daltoniam/SwiftHTTP/issues/182)). +Progress Closure. +[Global handlers](https://github.com/daltoniam/SwiftHTTP#global-handlers). + +#### [1.0.1](https://github.com/daltoniam/SwiftHTTP/tree/1.0.1) + +Custom header support in `HTTP` factory methods. + +#### [1.0.1](https://github.com/daltoniam/SwiftHTTP/tree/1.0.1) + +Custom header support in `HTTP` factory methods. + +#### [1.0.0](https://github.com/daltoniam/SwiftHTTP/tree/1.0.0) + +API refactor and first release of Swift 2 support. diff --git a/HTTPRequestSerializer.swift b/HTTPRequestSerializer.swift deleted file mode 100644 index a9e3e8d..0000000 --- a/HTTPRequestSerializer.swift +++ /dev/null @@ -1,286 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// HTTPRequestSerializer.swift -// -// Created by Dalton Cherry on 6/3/14. -// Copyright (c) 2014 Vluxe. All rights reserved. -// -////////////////////////////////////////////////////////////////////////////////////////////////// - -import Foundation - - -extension String { - /** - A simple extension to the String object to encode it for web request. - - :returns: Encoded version of of string it was called as. - */ - var escaped: String { - return CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,self,"[].",":/?&=;+!@#$()',*",CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)) as! String - } -} - -/// Default Serializer for serializing an object to an HTTP request. This applies to form serialization, parameter encoding, etc. -public class HTTPRequestSerializer: NSObject { - let contentTypeKey = "Content-Type" - - /// headers for the request. - public var headers = Dictionary() - /// encoding for the request. - public var stringEncoding: UInt = NSUTF8StringEncoding - /// Send request if using cellular network or not. Defaults to true. - public var allowsCellularAccess = true - /// If the request should handle cookies of not. Defaults to true. - public var HTTPShouldHandleCookies = true - /// If the request should use piplining or not. Defaults to false. - public var HTTPShouldUsePipelining = false - /// How long the timeout interval is. Defaults to 60 seconds. - public var timeoutInterval: NSTimeInterval = 60 - /// Set the request cache policy. Defaults to UseProtocolCachePolicy. - public var cachePolicy: NSURLRequestCachePolicy = NSURLRequestCachePolicy.UseProtocolCachePolicy - /// Set the network service. Defaults to NetworkServiceTypeDefault. - public var networkServiceType = NSURLRequestNetworkServiceType.NetworkServiceTypeDefault - - /// Initializes a new HTTPRequestSerializer Object. - public override init() { - super.init() - } - - /** - Creates a new NSMutableURLRequest object with configured options. - - :param: url The url you would like to make a request to. - :param: method The HTTP method/verb for the request. - - :returns: A new NSMutableURLRequest with said options. - */ - public func newRequest(url: NSURL, method: HTTPMethod) -> NSMutableURLRequest { - var request = NSMutableURLRequest(URL: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) - request.HTTPMethod = method.rawValue - request.cachePolicy = self.cachePolicy - request.timeoutInterval = self.timeoutInterval - request.allowsCellularAccess = self.allowsCellularAccess - request.HTTPShouldHandleCookies = self.HTTPShouldHandleCookies - request.HTTPShouldUsePipelining = self.HTTPShouldUsePipelining - request.networkServiceType = self.networkServiceType - for (key,val) in self.headers { - request.addValue(val, forHTTPHeaderField: key) - } - return request - } - - /** - Creates a new NSMutableURLRequest object with configured options. - - :param: url The url you would like to make a request to. - :param: method The HTTP method/verb for the request. - :param: parameters The parameters are HTTP parameters you would like to send. - - :returns: A new NSMutableURLRequest with said options or an error. - */ - public func createRequest(url: NSURL, method: HTTPMethod, parameters: Dictionary?) -> (request: NSURLRequest, error: NSError?) { - - var request = newRequest(url, method: method) - var isMulti = false - //do a check for upload objects to see if we are multi form - if let params = parameters { - isMulti = isMultiForm(params) - } - if isMulti { - if(method != .POST && method != .PUT && method != .PATCH) { - request.HTTPMethod = HTTPMethod.POST.rawValue // you probably wanted a post - } - var boundary = "Boundary+\(arc4random())\(arc4random())" - if parameters != nil { - request.HTTPBody = dataFromParameters(parameters!,boundary: boundary) - } - if request.valueForHTTPHeaderField(contentTypeKey) == nil { - request.setValue("multipart/form-data; boundary=\(boundary)", - forHTTPHeaderField:contentTypeKey) - } - return (request,nil) - } - var queryString = "" - if parameters != nil { - queryString = self.stringFromParameters(parameters!) - } - if isURIParam(method) { - var para = (request.URL!.query != nil) ? "&" : "?" - var newUrl = "\(request.URL!.absoluteString!)" - if count(queryString) > 0 { - newUrl += "\(para)\(queryString)" - } - request.URL = NSURL(string: newUrl) - } else { - var charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding)); - if request.valueForHTTPHeaderField(contentTypeKey) == nil { - request.setValue("application/x-www-form-urlencoded; charset=\(charset)", - forHTTPHeaderField:contentTypeKey) - } - request.HTTPBody = queryString.dataUsingEncoding(self.stringEncoding) - } - return (request,nil) - } - - ///check for multi form objects - public func isMultiForm(params: Dictionary) -> Bool { - for (name,object: AnyObject) in params { - if object is HTTPUpload { - return true - } else if let subParams = object as? Dictionary { - if isMultiForm(subParams) { - return true - } - } - } - return false - } - - ///check if enum is a HTTPMethod that requires the params in the URL - public func isURIParam(method: HTTPMethod) -> Bool { - if(method == .GET || method == .HEAD || method == .DELETE) { - return true - } - return false - } - - ///convert the parameter dict to its HTTP string representation - public func stringFromParameters(parameters: Dictionary) -> String { - return join("&", map(serializeObject(parameters, key: nil), {(pair) in - return pair.stringValue() - })) - } - - ///the method to serialized all the objects - func serializeObject(object: AnyObject,key: String?) -> Array { - var collect = Array() - if let array = object as? Array { - for nestedValue : AnyObject in array { - collect.extend(self.serializeObject(nestedValue,key: "\(key!)[]")) - } - } else if let dict = object as? Dictionary { - for (nestedKey, nestedObject: AnyObject) in dict { - var newKey = key != nil ? "\(key!)[\(nestedKey)]" : nestedKey - collect.extend(self.serializeObject(nestedObject,key: newKey)) - } - } else { - collect.append(HTTPPair(value: object, key: key)) - } - return collect - } - - //create a multi form data object of the parameters - func dataFromParameters(parameters: Dictionary,boundary: String) -> NSData { - var mutData = NSMutableData() - var multiCRLF = "\r\n" - var boundSplit = "\(multiCRLF)--\(boundary)\(multiCRLF)".dataUsingEncoding(self.stringEncoding)! - var lastBound = "\(multiCRLF)--\(boundary)--\(multiCRLF)".dataUsingEncoding(self.stringEncoding)! - mutData.appendData("--\(boundary)\(multiCRLF)".dataUsingEncoding(self.stringEncoding)!) - - let pairs = serializeObject(parameters, key: nil) - let count = pairs.count-1 - var i = 0 - for pair in pairs { - var append = true - if let upload = pair.getUpload() { - if let data = upload.data { - mutData.appendData(multiFormHeader(pair.k, fileName: upload.fileName, - type: upload.mimeType, multiCRLF: multiCRLF).dataUsingEncoding(self.stringEncoding)!) - mutData.appendData(data) - } else { - append = false - } - } else { - let str = "\(multiFormHeader(pair.k, fileName: nil, type: nil, multiCRLF: multiCRLF))\(pair.getValue())" - mutData.appendData(str.dataUsingEncoding(self.stringEncoding)!) - } - if append { - if i == count { - mutData.appendData(lastBound) - } else { - mutData.appendData(boundSplit) - } - } - i++ - } - return mutData - } - - ///helper method to create the multi form headers - func multiFormHeader(name: String, fileName: String?, type: String?, multiCRLF: String) -> String { - var str = "Content-Disposition: form-data; name=\"\(name.escaped)\"" - if fileName != nil { - str += "; filename=\"\(fileName!)\"" - } - str += multiCRLF - if type != nil { - str += "Content-Type: \(type!)\(multiCRLF)" - } - str += multiCRLF - return str - } - - /// Creates key/pair of the parameters. - class HTTPPair: NSObject { - var val: AnyObject - var k: String! - - init(value: AnyObject, key: String?) { - self.val = value - self.k = key - } - - func getUpload() -> HTTPUpload? { - return self.val as? HTTPUpload - } - - func getValue() -> String { - var val = "" - if let str = self.val as? String { - val = str - } else if self.val.description != nil { - val = self.val.description - } - return val - } - - func stringValue() -> String { - var v = getValue() - if self.k == nil { - return v.escaped - } - return "\(self.k.escaped)=\(v.escaped)" - } - - } - -} - -/// JSON Serializer for serializing an object to an HTTP request. Same as HTTPRequestSerializer, expect instead of HTTP form encoding it does JSON. -public class JSONRequestSerializer: HTTPRequestSerializer { - - /** - Creates a new NSMutableURLRequest object with configured options. - - :param: url The url you would like to make a request to. - :param: method The HTTP method/verb for the request. - :param: parameters The parameters are HTTP parameters you would like to send. - - :returns: A new NSMutableURLRequest with said options or an error. - */ - public override func createRequest(url: NSURL, method: HTTPMethod, parameters: Dictionary?) -> (request: NSURLRequest, error: NSError?) { - if self.isURIParam(method) { - return super.createRequest(url, method: method, parameters: parameters) - } - var request = newRequest(url, method: method) - var error: NSError? - if parameters != nil { - var charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); - request.setValue("application/json; charset=\(charset)", forHTTPHeaderField: self.contentTypeKey) - request.HTTPBody = NSJSONSerialization.dataWithJSONObject(parameters!, options: NSJSONWritingOptions(), error:&error) - } - return (request, error) - } - -} diff --git a/HTTPResponseSerializer.swift b/HTTPResponseSerializer.swift deleted file mode 100644 index f775fcd..0000000 --- a/HTTPResponseSerializer.swift +++ /dev/null @@ -1,36 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// HTTPResponseSerializer.swift -// -// Created by Dalton Cherry on 6/16/14. -// Copyright (c) 2014 Vluxe. All rights reserved. -// -////////////////////////////////////////////////////////////////////////////////////////////////// - -import Foundation - -/// This protocol provides a way to implement a custom serializer. -public protocol HTTPResponseSerializer { - /// This can be used if you want to have your data parsed/serialized into something instead of just a NSData blob. - func responseObjectFromResponse(response: NSURLResponse, data: NSData) -> (object: AnyObject?, error: NSError?) -} - -/// Serialize the data into a JSON object. -public struct JSONResponseSerializer : HTTPResponseSerializer { - /// Initializes a new JSONResponseSerializer Object. - public init(){} - - /** - Creates a HTTPOperation that can be scheduled on a NSOperationQueue. Called by convenience HTTP verb methods below. - - :param: response The NSURLResponse. - :param: data The response data to be parsed into JSON. - - :returns: Returns a object from JSON data and an NSError if an error occured while parsing the data. - */ - public func responseObjectFromResponse(response: NSURLResponse, data: NSData) -> (object: AnyObject?, error: NSError?) { - var error: NSError? - let response: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(), error: &error) - return (response,error) - } -} diff --git a/HTTPSecurity.swift b/HTTPSecurity.swift deleted file mode 100644 index 53101d1..0000000 --- a/HTTPSecurity.swift +++ /dev/null @@ -1,244 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// HTTPSecurity.swift -// SwiftHTTP -// -// Created by Dalton Cherry on 5/13/15. -// Copyright (c) 2015 Vluxe. All rights reserved. -// -////////////////////////////////////////////////////////////////////////////////////////////////// - -import Foundation -import Security - -public class HTTPSSLCert { - var certData: NSData? - var key: SecKeyRef? - - /** - Designated init for certificates - - :param: data is the binary data of the certificate - - :returns: a representation security object to be used with - */ - public init(data: NSData) { - self.certData = data - } - - /** - Designated init for public keys - - :param: key is the public key to be used - - :returns: a representation security object to be used with - */ - public init(key: SecKeyRef) { - self.key = key - } -} - -public class HTTPSecurity { - public var validatedDN = true //should the domain name be validated? - - var isReady = false //is the key processing done? - var certificates: [NSData]? //the certificates - var pubKeys: [SecKeyRef]? //the public keys - var usePublicKeys = false //use public keys or certificate validation? - - /** - Use certs from main app bundle - - :param: usePublicKeys is to specific if the publicKeys or certificates should be used for SSL pinning validation - - :returns: a representation security object to be used with - */ - public convenience init(usePublicKeys: Bool = false) { - let paths = NSBundle.mainBundle().pathsForResourcesOfType("cer", inDirectory: ".") - var collect = Array() - for path in paths { - if let d = NSData(contentsOfFile: path as! String) { - collect.append(HTTPSSLCert(data: d)) - } - } - self.init(certs:collect, usePublicKeys: usePublicKeys) - } - - /** - Designated init - - :param: keys is the certificates or public keys to use - :param: usePublicKeys is to specific if the publicKeys or certificates should be used for SSL pinning validation - - :returns: a representation security object to be used with - */ - public init(certs: [HTTPSSLCert], usePublicKeys: Bool) { - self.usePublicKeys = usePublicKeys - - if self.usePublicKeys { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), { - var collect = Array() - for cert in certs { - if let data = cert.certData where cert.key == nil { - cert.key = self.extractPublicKey(data) - } - if let k = cert.key { - collect.append(k) - } - } - self.pubKeys = collect - self.isReady = true - }) - } else { - var collect = Array() - for cert in certs { - if let d = cert.certData { - collect.append(d) - } - } - self.certificates = collect - self.isReady = true - } - } - - /** - Valid the trust and domain name. - - :param: trust is the serverTrust to validate - :param: domain is the CN domain to validate - - :returns: if the key was successfully validated - */ - public func isValid(trust: SecTrustRef, domain: String?) -> Bool { - - var tries = 0 - while(!self.isReady) { - usleep(1000) - tries += 1 - if tries > 5 { - return false //doesn't appear it is going to ever be ready... - } - } - var policy: SecPolicyRef - if self.validatedDN { - policy = SecPolicyCreateSSL(1, domain).takeRetainedValue() - } else { - policy = SecPolicyCreateBasicX509().takeRetainedValue() - } - SecTrustSetPolicies(trust,policy) - if self.usePublicKeys { - if let keys = self.pubKeys { - var trustedCount = 0 - let serverPubKeys = publicKeyChainForTrust(trust) - for serverKey in serverPubKeys as [AnyObject] { - for key in keys as [AnyObject] { - if serverKey.isEqual(key) { - trustedCount++ - break - } - } - } - if trustedCount == serverPubKeys.count { - return true - } - } - } else if let certs = self.certificates { - let serverCerts = certificateChainForTrust(trust) - var collect = Array() - for cert in certs { - collect.append(SecCertificateCreateWithData(nil,cert).takeRetainedValue()) - } - SecTrustSetAnchorCertificates(trust,collect) - var result: SecTrustResultType = 0 - SecTrustEvaluate(trust,&result) - let r = Int(result) - if r == kSecTrustResultUnspecified || r == kSecTrustResultProceed { - var trustedCount = 0 - for serverCert in serverCerts { - for cert in certs { - if cert == serverCert { - trustedCount++ - break - } - } - } - if trustedCount == serverCerts.count { - return true - } - } - } - return false - } - - /** - Get the public key from a certificate data - - :param: data is the certificate to pull the public key from - - :returns: a public key - */ - func extractPublicKey(data: NSData) -> SecKeyRef? { - var publicKey: NSData? - let possibleCert = SecCertificateCreateWithData(nil,data) - if let cert = possibleCert { - return extractPublicKeyFromCert(cert.takeRetainedValue(),policy: SecPolicyCreateBasicX509().takeRetainedValue()) - } - return nil - } - - /** - Get the public key from a certificate - - :param: data is the certificate to pull the public key from - - :returns: a public key - */ - func extractPublicKeyFromCert(cert: SecCertificate, policy: SecPolicy) -> SecKeyRef? { - var possibleTrust: Unmanaged? - SecTrustCreateWithCertificates(cert,policy, &possibleTrust) - if let trust = possibleTrust { - let t = trust.takeRetainedValue() - var result: SecTrustResultType = 0 - SecTrustEvaluate(t,&result) - return SecTrustCopyPublicKey(t).takeRetainedValue() - } - return nil - } - - /** - Get the certificate chain for the trust - - :param: trust is the trust to lookup the certificate chain for - - :returns: the certificate chain for the trust - */ - func certificateChainForTrust(trust: SecTrustRef) -> Array { - var collect = Array() - for var i = 0; i < SecTrustGetCertificateCount(trust); i++ { - let cert = SecTrustGetCertificateAtIndex(trust,i) - collect.append(SecCertificateCopyData(cert.takeRetainedValue()).takeRetainedValue()) - } - return collect - } - - /** - Get the public key chain for the trust - - :param: trust is the trust to lookup the certificate chain and extract the public keys - - :returns: the public keys from the certifcate chain for the trust - */ - func publicKeyChainForTrust(trust: SecTrustRef) -> Array { - var collect = Array() - let policy = SecPolicyCreateBasicX509().takeRetainedValue() - for var i = 0; i < SecTrustGetCertificateCount(trust); i++ { - let cert = SecTrustGetCertificateAtIndex(trust,i) - if let key = extractPublicKeyFromCert(cert.takeRetainedValue(), policy: policy) { - collect.append(key) - } - } - return collect - } - - -} \ No newline at end of file diff --git a/HTTPTask.swift b/HTTPTask.swift deleted file mode 100644 index 2142a9b..0000000 --- a/HTTPTask.swift +++ /dev/null @@ -1,556 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// HTTPTask.swift -// -// Created by Dalton Cherry on 6/3/14. -// Copyright (c) 2014 Vluxe. All rights reserved. -// -////////////////////////////////////////////////////////////////////////////////////////////////// - -import Foundation - -/// HTTP Verbs. -/// -/// - GET: For GET requests. -/// - POST: For POST requests. -/// - PUT: For PUT requests. -/// - HEAD: For HEAD requests. -/// - DELETE: For DELETE requests. -/// - PATCH: For PATCH requests. -public enum HTTPMethod: String { - case GET = "GET" - case POST = "POST" - case PUT = "PUT" - case HEAD = "HEAD" - case DELETE = "DELETE" - case PATCH = "PATCH" -} - -/// Object representation of a HTTP Response. -public class HTTPResponse { - /// The header values in HTTP response. - public var headers: Dictionary? - /// The mime type of the HTTP response. - public var mimeType: String? - /// The suggested filename for a downloaded file. - public var suggestedFilename: String? - /// The body or response data of the HTTP response. - public var responseObject: AnyObject? - /// The status code of the HTTP response. - public var statusCode: Int? - /// The URL of the HTTP response. - public var URL: NSURL? - /// The Error of the HTTP response (if there was one). - public var error: NSError? - ///Returns the response as a string - public var text: String? { - if let d = self.responseObject as? NSData { - return NSString(data: d, encoding: NSUTF8StringEncoding) as? String - } else if let val: AnyObject = self.responseObject { - return "\(val)" - } - return nil - } - //get the description of the response - public var description: String { - var buffer = "" - if let u = self.URL { - buffer += "URL:\n\(u)\n\n" - } - if let code = self.statusCode { - buffer += "Status Code:\n\(code)\n\n" - } - if let heads = self.headers { - buffer += "Headers:\n" - for (key, value) in heads { - buffer += "\(key): \(value)\n" - } - buffer += "\n" - } - if let s = self.text { - buffer += "Payload:\n\(s)\n" - } - return buffer - } -} - -/// Holds the blocks of the background task. -class BackgroundBlocks { - // these 2 only get used for background download/upload since they have to be delegate methods - var completionHandler:((HTTPResponse) -> Void)? - var progress:((Double) -> Void)? - - /** - Initializes a new Background Block - - :param: completionHandler The closure that is run when a HTTP Request finished. - :param: progress The closure that is run on the progress of a HTTP Upload or Download. - */ - init(_ completionHandler: ((HTTPResponse) -> Void)?,_ progress: ((Double) -> Void)?) { - self.completionHandler = completionHandler - self.progress = progress - } -} - -/// Subclass of NSOperation for handling and scheduling HTTPTask on a NSOperationQueue. -public class HTTPOperation : NSOperation { - private var task: NSURLSessionDataTask! - private var running = false - - /// Controls if the task is finished or not. - private var done = false - - //MARK: Subclassed NSOperation Methods - - /// Returns if the task is asynchronous or not. NSURLSessionTask requests are asynchronous. - override public var asynchronous: Bool { - return true - } - - /// Returns if the task is current running. - override public var executing: Bool { - return running - } - - /// Returns if the task is finished. - override public var finished: Bool { - return done - } - - /// Starts the task. - override public func start() { - if cancelled { - self.willChangeValueForKey("isFinished") - done = true - self.didChangeValueForKey("isFinished") - return - } - - self.willChangeValueForKey("isExecuting") - self.willChangeValueForKey("isFinished") - - running = true - done = false - - self.didChangeValueForKey("isExecuting") - self.didChangeValueForKey("isFinished") - - task.resume() - } - - /// Cancels the running task. - override public func cancel() { - super.cancel() - task.cancel() - } - - /// Sets the task to finished. - public func finish() { - self.willChangeValueForKey("isExecuting") - self.willChangeValueForKey("isFinished") - - running = false - done = true - - self.didChangeValueForKey("isExecuting") - self.didChangeValueForKey("isFinished") - } -} - -/// Configures NSURLSession Request for HTTPOperation. Also provides convenience methods for easily running HTTP Request. -public class HTTPTask : NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate { - var backgroundTaskMap = Dictionary() - //var sess: NSURLSession? - - public var baseURL: String? - public var requestSerializer = HTTPRequestSerializer() - public var responseSerializer: HTTPResponseSerializer? - //This gets called on auth challenges. If nil, default handling is use. - //Returning nil from this method will cause the request to be rejected and cancelled - public var auth:((NSURLAuthenticationChallenge) -> NSURLCredential?)? - - //This is for doing SSL pinning - public var security: HTTPSecurity? - - //MARK: Public Methods - - /// A newly minted HTTPTask for your enjoyment. - public override init() { - super.init() - } - - /** - Creates a HTTPOperation that can be scheduled on a NSOperationQueue. Called by convenience HTTP verb methods below. - - :param: url The url you would like to make a request to. - :param: method The HTTP method/verb for the request. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - - :returns: A freshly constructed HTTPOperation to add to your NSOperationQueue. - */ - public func create(url: String, method: HTTPMethod, parameters: Dictionary!, completionHandler:((HTTPResponse) -> Void)!) -> HTTPOperation? { - - var serialResponse = HTTPResponse() - let serialReq = createRequest(url, method: method, parameters: parameters) - if let err = serialReq.error { - if let handler = completionHandler { - serialResponse.error = err - handler(serialResponse) - } - return nil - } - let opt = HTTPOperation() - let config = NSURLSessionConfiguration.defaultSessionConfiguration() - let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil) - let task = session.dataTaskWithRequest(serialReq.request, - completionHandler: {(data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in - if let handler = completionHandler { - if let hresponse = response as? NSHTTPURLResponse { - serialResponse.headers = hresponse.allHeaderFields as? Dictionary - serialResponse.mimeType = hresponse.MIMEType - serialResponse.suggestedFilename = hresponse.suggestedFilename - serialResponse.statusCode = hresponse.statusCode - serialResponse.URL = hresponse.URL - } - serialResponse.error = error - if let d = data { - serialResponse.responseObject = d - if let resSerializer = self.responseSerializer, let resp = response { - let resObj = resSerializer.responseObjectFromResponse(resp, data: d) - serialResponse.responseObject = resObj.object - serialResponse.error = resObj.error - } - if let code = serialResponse.statusCode where serialResponse.statusCode > 299 { - serialResponse.error = self.createError(code) - } - } - handler(serialResponse) - } - opt.finish() - }) - opt.task = task - return opt - } - - /** - Creates a HTTPOperation as a HTTP GET request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func GET(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.GET, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - /** - Creates a HTTPOperation as a HTTP POST request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func POST(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.POST, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - /** - Creates a HTTPOperation as a HTTP PATCH request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func PATCH(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.PATCH, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - - /** - Creates a HTTPOperation as a HTTP PUT request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func PUT(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.PUT, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - /** - Creates a HTTPOperation as a HTTP DELETE request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func DELETE(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.DELETE, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - /** - Creates a HTTPOperation as a HTTP HEAD request and starts it for you. - - :param: url The url you would like to make a request to. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func HEAD(url: String, parameters: Dictionary?, completionHandler:((HTTPResponse) -> Void)!) { - if let opt = self.create(url, method:.HEAD, parameters: parameters,completionHandler: completionHandler) { - opt.start() - } - } - - /** - Creates and starts a HTTPOperation to download a file in the background. - - :param: url The url you would like to make a request to. - :param: method The HTTP method you want to use. Default is GET. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: progress The progress returned in the progress closure is between 0 and 1. - :param: completionHandler The closure that is run when the HTTP Request finishes. The HTTPResponse responseObject object will be a fileURL. You MUST copy the fileURL return in HTTPResponse.responseObject to a new location before using it (e.g. your documents directory). - */ - public func download(url: String, method: HTTPMethod = .GET, parameters: Dictionary?,progress:((Double) -> Void)!, completionHandler:((HTTPResponse) -> Void)!) -> NSURLSessionDownloadTask? { - let serialReq = createRequest(url,method: method, parameters: parameters) - if let err = serialReq.error { - if let handler = completionHandler { - var res = HTTPResponse() - res.error = err - handler(res) - } - return nil - } - let ident = createBackgroundIdent() - let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(ident) - let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil) - let task = session.downloadTaskWithRequest(serialReq.request) - backgroundTaskMap[ident] = BackgroundBlocks(completionHandler,progress) - //this does not have to be queueable as Apple's background dameon *should* handle that. - task.resume() - return task - } - - /** - Creates and starts a HTTPOperation to upload a file in the background. - - :param: url The url you would like to make a request to. - :param: method The HTTP method you want to use. Default is POST. - :param: parameters The parameters are HTTP parameters you would like to send. - :param: progress The progress returned in the progress closure is between 0 and 1. - :param: completionHandler The closure that is run when a HTTP Request finished. - */ - public func upload(url: String, method: HTTPMethod = .POST, parameters: Dictionary?,progress:((Double) -> Void)!, completionHandler:((HTTPResponse) -> Void)!) -> NSURLSessionTask? { - let serialReq = createRequest(url,method: method, parameters: parameters) - if let err = serialReq.error { - if let handler = completionHandler { - var res = HTTPResponse() - res.error = err - handler(res) - } - return nil - } - let ident = createBackgroundIdent() - let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(ident) - let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil) - let task = session.uploadTaskWithStreamedRequest(serialReq.request) - backgroundTaskMap[ident] = BackgroundBlocks(completionHandler,progress) - task.resume() - return task - } - - //MARK: Private Helper Methods - - /** - Creates and starts a HTTPOperation to download a file in the background. - - :param: url The url you would like to make a request to. - :param: method The HTTP method/verb for the request. - :param: parameters The parameters are HTTP parameters you would like to send. - - :returns: A NSURLRequest from configured requestSerializer. - */ - private func createRequest(url: String, method: HTTPMethod, parameters: Dictionary!) -> (request: NSURLRequest, error: NSError?) { - var urlVal = url - //probably should change the 'http' to something more generic - if let base = self.baseURL where !url.hasPrefix("http") { - var split = url.hasPrefix("/") ? "" : "/" - urlVal = "\(base)\(split)\(url)" - } - if let encoded = urlVal.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) { - if let u = NSURL(string: encoded) { - return self.requestSerializer.createRequest(u, method: method, parameters: parameters) - } - } - return (NSURLRequest(),createError(-1001)) - } - - /** - Creates a random string to use for the identifier of the background download/upload requests. - - :returns: Identifier String. - */ - private func createBackgroundIdent() -> String { - let letters = "abcdefghijklmnopqurstuvwxyz" - var str = "" - for var i = 0; i < 14; i++ { - let start = Int(arc4random() % 14) - str.append(letters[advance(letters.startIndex,start)]) - } - return "com.vluxe.swifthttp.request.\(str)" - } - - /** - Creates a random string to use for the identifier of the background download/upload requests. - - :param: code Code for error. - - :returns: An NSError. - */ - private func createError(code: Int) -> NSError { - var text = "An error occured" - if code == 404 { - text = "Page not found" - } else if code == 401 { - text = "Access denied" - } else if code == -1001 { - text = "Invalid URL" - } - return NSError(domain: "HTTPTask", code: code, userInfo: [NSLocalizedDescriptionKey: text]) - } - - - /** - Creates a random string to use for the identifier of the background download/upload requests. - - :param: identifier The identifier string. - - :returns: An NSError. - */ - private func cleanupBackground(identifier: String) { - backgroundTaskMap.removeValueForKey(identifier) - } - - //MARK: NSURLSession Delegate Methods - - /// Method for authentication challenge. - public func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void) { - if let sec = security where challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - let space = challenge.protectionSpace - if let trust = space.serverTrust { - if sec.isValid(trust, domain: space.host) { - completionHandler(.UseCredential, NSURLCredential(trust: trust)) - return - } - } - completionHandler(.CancelAuthenticationChallenge, nil) - return - - } else if let a = auth { - let cred = a(challenge) - if let c = cred { - completionHandler(.UseCredential, c) - return - } - completionHandler(.RejectProtectionSpace, nil) - return - } - completionHandler(.PerformDefaultHandling, nil) - } - - //MARK: Methods for background download/upload - - ///update the download/upload progress closure - func handleProgress(session: NSURLSession, totalBytesExpected: Int64, currentBytes: Int64) { - if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168 - let increment = 100.0/Double(totalBytesExpected) - var current = (increment*Double(currentBytes))*0.01 - if current > 1 { - current = 1; - } - if let blocks = backgroundTaskMap[session.configuration.identifier] { - if blocks.progress != nil { - blocks.progress!(current) - } - } - } - } - - //call the completionHandler closure for upload/download requests - func handleFinish(session: NSURLSession, task: NSURLSessionTask, response: AnyObject) { - if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168 - if let blocks = backgroundTaskMap[session.configuration.identifier] { - if let handler = blocks.completionHandler { - var resp = HTTPResponse() - if let hresponse = task.response as? NSHTTPURLResponse { - resp.headers = hresponse.allHeaderFields as? Dictionary - resp.mimeType = hresponse.MIMEType - resp.suggestedFilename = hresponse.suggestedFilename - resp.statusCode = hresponse.statusCode - resp.URL = hresponse.URL - } - resp.responseObject = response - if let code = resp.statusCode where resp.statusCode > 299 { - resp.error = self.createError(code) - } - handler(resp) - } - } - cleanupBackground(session.configuration.identifier) - } - } - - /// Called when the background task failed. - public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { - if let err = error { - if session.configuration.valueForKey("identifier") != nil { //temp workaround for radar: 21097168 - if let blocks = backgroundTaskMap[session.configuration.identifier] { - if let handler = blocks.completionHandler { - var res = HTTPResponse() - res.error = err - handler(res) - } - } - cleanupBackground(session.configuration.identifier) - } - } - } - - /// The background download finished and reports the url the data was saved to. - func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL!) { - handleFinish(session, task: downloadTask, response: location) - } - - /// Will report progress of background download - func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { - handleProgress(session, totalBytesExpected: totalBytesExpectedToWrite, currentBytes:totalBytesWritten) - } - - /// The background download finished, don't have to really do anything. - public func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) { - } - - /// The background upload finished and reports the response. - func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData!) { - handleFinish(session, task: dataTask, response: data) - } - - ///Will report progress of background upload - public func URLSession(session: NSURLSession, task: NSURLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - handleProgress(session, totalBytesExpected: totalBytesExpectedToSend, currentBytes:totalBytesSent) - } - - //implement if we want to support partial file upload/download - func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { - } -} diff --git a/HTTPUpload.swift b/HTTPUpload.swift deleted file mode 100644 index 9671634..0000000 --- a/HTTPUpload.swift +++ /dev/null @@ -1,96 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// HTTPUpload.swift -// -// Created by Dalton Cherry on 6/5/14. -// Copyright (c) 2014 Vluxe. All rights reserved. -// -////////////////////////////////////////////////////////////////////////////////////////////////// - -import Foundation - -#if os(iOS) - import MobileCoreServices -#endif - - -/// Object representation of a HTTP File Upload. -public class HTTPUpload: NSObject, NSCoding { - var fileUrl: NSURL? { - didSet { - updateMimeType() - loadData() - } - } - var mimeType: String? - var data: NSData? - var fileName: String? - - /// Tries to determine the mime type from the fileUrl extension. - func updateMimeType() { - if mimeType == nil && fileUrl != nil { - var UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileUrl?.pathExtension as NSString?, nil); - var str = UTTypeCopyPreferredTagWithClass(UTI.takeUnretainedValue(), kUTTagClassMIMEType); - if (str == nil) { - mimeType = "application/octet-stream"; - } else { - mimeType = str.takeUnretainedValue() as String - } - } - } - - /// loads the fileUrl into memory. - func loadData() { - if let url = fileUrl { - self.fileName = url.lastPathComponent - self.data = NSData(contentsOfURL: url, options: NSDataReadingOptions.DataReadingMappedIfSafe, error: nil) - } - } - - public func encodeWithCoder(aCoder: NSCoder) { - aCoder.encodeObject(self.fileUrl, forKey: "fileUrl") - aCoder.encodeObject(self.mimeType, forKey: "mimeType") - aCoder.encodeObject(self.fileName, forKey: "fileName") - aCoder.encodeObject(self.data, forKey: "data") - } - - /// Initializes a new HTTPUpload Object. - public override init() { - super.init() - } - - required public convenience init(coder aDecoder: NSCoder) { - self.init() - self.fileUrl = aDecoder.decodeObjectForKey("fileUrl") as? NSURL - self.mimeType = aDecoder.decodeObjectForKey("mimeType") as? String - self.fileName = aDecoder.decodeObjectForKey("fileName") as? String - self.data = aDecoder.decodeObjectForKey("data") as? NSData - } - - /** - Initializes a new HTTPUpload Object with a fileUrl. The fileName and mimeType will be infered. - - :param: fileUrl The fileUrl is a standard url path to a file. - */ - public convenience init(fileUrl: NSURL) { - self.init() - self.fileUrl = fileUrl - updateMimeType() - loadData() - } - - /** - Initializes a new HTTPUpload Object with a data blob of a file. The fileName and mimeType will be infered if none are provided. - - :param: data The data is a NSData representation of a file's data. - :param: fileName The fileName is just that. The file's name. - :param: mimeType The mimeType is just that. The mime type you would like the file to uploaded as. - */ - ///upload a file from a a data blob. Must add a filename and mimeType as that can't be infered from the data - public convenience init(data: NSData, fileName: String, mimeType: String) { - self.init() - self.data = data - self.fileName = fileName - self.mimeType = mimeType - } -} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..c806524 --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// +// Package.Swift +// SwiftHTTP +// +// Created by Dalton Cherry on 5/16/15. +// Copyright (c) 2014-2015 Dalton Cherry. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import PackageDescription + +let package = Package( + name: "SwiftHTTP" +) \ No newline at end of file diff --git a/README.md b/README.md index 728a407..b377211 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,12 @@ SwiftHTTP is a thin wrapper around NSURLSession in Swift to simplify HTTP reques ## Features - Convenient Closure APIs -- NSOperationQueue Support +- Simple Queue Support - Parameter Encoding -- Custom Response Serializer -- Builtin JSON Response Serialization +- Builtin JSON Request Serialization - Upload/Download with Progress Closure -- Concise Codebase. Under 1000 LOC +- Concise Codebase. -Full article here: [http://vluxe.io/swifthttp.html](http://vluxe.io/swifthttp.html) First thing is to import the framework. See the Installation instructions on how to add the framework to your project. @@ -25,163 +23,92 @@ import SwiftHTTP ### GET -The most basic request. By default an NSData object will be returned for the response. +The most basic request. By default an Data object will be returned for the response. ```swift -var request = HTTPTask() -request.GET("http://vluxe.io", parameters: nil, completionHandler: {(response: HTTPResponse) in +HTTP.GET("https://google.com") { response in if let err = response.error { - println("error: \(err.localizedDescription)") + print("error: \(err.localizedDescription)") return //also notify app of failure as needed } - if let data = response.responseObject as? NSData { - let str = NSString(data: data, encoding: NSUTF8StringEncoding) - println("response: \(str)") //prints the HTML of the page - } -}) + print("opt finished: \(response.description)") + //print("data is: \(response.data)") access the response of the data with response.data +} ``` We can also add parameters as with standard container objects and they will be properly serialized to their respective HTTP equivalent. ```swift -var request = HTTPTask() -request.GET("http://google.com", parameters: ["param": "param1", "array": ["first array element","second","third"], "num": 23], completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") +//the url sent will be https://google.com?hello=world¶m2=value2 +HTTP.GET("https://google.com", parameters: ["hello": "world", "param2": "value2"]) { response in + if let err = response.error { + print("error: \(err.localizedDescription)") return //also notify app of failure as needed } - if let res: AnyObject = response.responseObject { - println("response: \(res)") - } -}) + print("opt finished: \(response.description)") +} ``` -The `HTTPResponse` contains all the common HTTP response data, such as the responseObject of the data and the headers of the response. +The `Response` contains all the common HTTP response data, such as the responseObject of the data and the headers of the response. -### POST +### HTTP Methods -A POST request is just as easy as a GET. +All the common HTTP methods are avalaible as convenience methods as well. + +### POST ```swift -var request = HTTPTask() -//we have to add the explicit type, else the wrong type is inferred. See the vluxe.io article for more info. -let params: Dictionary = ["param": "param1", "array": ["first array element","second","third"], "num": 23, "dict": ["someKey": "someVal"]] -request.POST("http://domain.com/create", parameters: params, completionHandler: {(response: HTTPResponse) in - //do things... -}) +let params = ["param": "param1", "array": ["first array element","second","third"], "num": 23, "dict": ["someKey": "someVal"]] +HTTP.POST("https://domain.com/new", parameters: params) { response in +//do things... +} ``` ### PUT -PUT works the same as post. The example also include a file upload to do a multi form request. - ```swift -let fileUrl = NSURL.fileURLWithPath("/Users/dalton/Desktop/file")! -var request = HTTPTask() -request.PUT("http://domain.com/1", parameters: ["param": "hi", "something": "else", "key": "value","file": HTTPUpload(fileUrl: fileUrl)], completionHandler: {(response: HTTPResponse) in - //do stuff -}) +HTTP.PUT("https://domain.com/1") ``` -The HTTPUpload object is use to represent files on disk or in memory file as data. - -### DELETE - -DELETE works the same as the GET. +### HEAD ```swift -var request = HTTPTask() -request.DELETE("http://domain.com/1", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("DELETE was successful!") -}) +HTTP.HEAD("https://domain.com/1") ``` -### HEAD - -HEAD works the same as the GET. +### DELETE ```swift -var request = HTTPTask() -request.HEAD("http://domain.com/image.png", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("The file does exist!") -}) +HTTP.DELETE("https://domain.com/1") ``` ### Download -The download method uses the background download functionality of NSURLSession. It also has a progress closure to report the progress of the download. - ```swift -var request = HTTPTask() -let downloadTask = request.download("http://vluxe.io/assets/images/logo.png", parameters: nil, progress: {(complete: Double) in - println("percent complete: \(complete)") - }, completionHandler: {(response: HTTPResponse) in - println("download finished!") - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let url = response.responseObject as? NSURL { - //we MUST copy the file from its temp location to a permanent location. - if let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as? String { - if let fileName = response.suggestedFilename { - if let newPath = NSURL(fileURLWithPath: "\(path)/\(fileName)") { - let fileManager = NSFileManager.defaultManager() - fileManager.removeItemAtURL(newPath, error: nil) - fileManager.moveItemAtURL(url, toURL: newPath, error:nil) - } - } - } - } - +HTTP.Download("http://www.cbu.edu.zm/downloads/pdf-sample.pdf", completion: { (response, url) in + //move the temp file to desired location... }) ``` -Cancel the download. - -```swift -if let t = downloadTask { - t.cancel() -} -``` - ### Upload -File uploads can be done using the `HTTPUpload` object. All files to upload should be wrapped in a HTTPUpload object and added as a parameter. +File uploads can be done using the `Upload` object. All files to upload should be wrapped in a Upload object and added as a parameter. ```swift -let task = HTTPTask() -var fileUrl = NSURL(fileURLWithPath: "/Users/dalton/Desktop/testfile")! -task.upload("http://domain.com/upload", method: .POST, parameters: ["aParam": "aValue", "file": HTTPUpload(fileUrl: fileUrl)], progress: { (value: Double) in - println("progress: \(value)") -}, completionHandler: { (response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let data = response.responseObject as? NSData { - let str = NSString(data: data, encoding: NSUTF8StringEncoding) - println("response: \(str!)") //prints the response - } -}) +let fileUrl = URL(fileURLWithPath: "/Users/dalton/Desktop/testfile")! +HTTP.POST("https://domain.com/new", parameters: ["aParam": "aValue", "file": Upload(fileUrl: fileUrl)]) { response in +//do things... +} ``` -`HTTPUpload` comes in both a on disk fileUrl version and a NSData version. +`Upload` comes in both a on disk fileUrl version and a Data version. ### Custom Headers -Custom HTTP headers can be add to a request via the requestSerializer. +Custom HTTP headers can be add to a request with the standard NSMutableRequest API: ```swift -var request = HTTPTask() -request.requestSerializer = HTTPRequestSerializer() -request.requestSerializer.headers["someKey"] = "SomeValue" //example of adding a header value +HTTP.GET("https://domain.com", parameters: ["hello": "there"], headers: ["header": "value"]) { response in + //do stuff +} ``` ### SSL Pinning @@ -189,172 +116,145 @@ request.requestSerializer.headers["someKey"] = "SomeValue" //example of adding a SSL Pinning is also supported in SwiftHTTP. ```swift -let task = HTTPTask() -let data = ... //load your certificate from disk +var req = URLRequest(urlString: "https://domain.com")! +req?.timeoutInterval = 5 +let task = HTTP(req) task.security = HTTPSecurity(certs: [HTTPSSLCert(data: data)], usePublicKeys: true) -//task.security = HTTPSecurity() //uses the .cer files in your app's bundle -request.GET("http://yourdomain.com", parameters: nil, completionHandler: {(response: HTTPResponse) in - //handle response -}) +//opt.security = HTTPSecurity() //uses the .cer files in your app's bundle +task.run { (response) in + if let err = response.error { + print("error: \(err.localizedDescription)") + return //also notify app of failure as needed + } + print("opt finished: \(response.description)") +} ``` -You load either a `NSData` blob of your certificate or you can use a `SecKeyRef` if you have a public key you want to use. The `usePublicKeys` bool is whether to use the certificates for validation or the public keys. The public keys will be extracted from the certificates automatically if `usePublicKeys` is choosen. +You load either a `Data` blob of your certificate or you can use a `SecKeyRef` if you have a public key you want to use. The `usePublicKeys` bool is whether to use the certificates for validation or the public keys. The public keys will be extracted from the certificates automatically if `usePublicKeys` is choosen. ### Authentication SwiftHTTP supports authentication through [NSURLCredential](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLCredential_Class/Reference/Reference.html). Currently only Basic Auth and Digest Auth have been tested. ```swift -var request = HTTPTask() +var req = URLRequest(urlString: "https://domain.com")! +req.timeoutInterval = 5 +let task = HTTP(req) //the auth closures will continually be called until a successful auth or rejection var attempted = false -request.auth = {(challenge: NSURLAuthenticationChallenge) in +task.auth = { challenge in if !attempted { attempted = true return NSURLCredential(user: "user", password: "passwd", persistence: .ForSession) } return nil //auth failed, nil causes the request to be properly cancelled. } -request.GET("http://httpbin.org/basic-auth/user/passwd", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("winning!") -}) +task.run { (response) in + //do stuff +} ``` Allow all certificates example: ```swift -var request = HTTPTask() +var req = URLRequest(urlString: "https://domain.com")! +req.timeoutInterval = 5 +let task = HTTP(req) +//the auth closures will continually be called until a successful auth or rejection var attempted = false -request.auth = {(challenge: NSURLAuthenticationChallenge) in +task.auth = { challenge in if !attempted { attempted = true return NSURLCredential(forTrust: challenge.protectionSpace.serverTrust) } - return nil + return nil //auth failed, nil causes the request to be properly cancelled. +} +task.run { (response) in + //do stuff } -request.GET("https://somedomain.com", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("winning!") -}) ``` +### Operation Queue -### BaseURL - -SwiftHTTP also supports use a request object with a baseURL. This is super handy for RESTFul API interaction. +SwiftHTTP also has a simple queue in it! ```swift -var request = HTTPTask() -request.baseURL = "http://api.someserver.com/1" -request.GET("/users", parameters: ["key": "value"], completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("Got data from http://api.someserver.com/1/users") -}) +let queue = HTTPQueue(maxSimultaneousRequest: 2) +var req = URLRequest(urlString: "https://google.com")! +req.timeoutInterval = 5 +let task = HTTP(req) +task.onFinish = { (response) in + print("item in the queue finished: \(response.URL!)") +} +queue.add(http: task) //the request will start running once added to the queue -request.POST("/users", parameters: ["key": "updatedVale"], completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("Got data from http://api.someserver.com/1/users") -}) -request.GET("/resources", parameters: ["key": "value"], completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - println("Got data from http://api.someserver.com/1/resources") -}) +var req2 = URLRequest(urlString: "https://apple.com")! +req2.timeoutInterval = 5 +let task2 = HTTP(req2) +task2.onFinish = { (response) in + print("item in the queue finished: \(response.URL!)") +} +queue.add(http: task2) + +//etc... + +queue.finished { + print("all items finished") +} ``` -### Operation Queue +### Cancel -Operation queues are also supported in SwiftHTTP. +Let's say you want to cancel the request a little later, call the `cancel` method. ```swift -let operationQueue = NSOperationQueue() -operationQueue.maxConcurrentOperationCount = 2 -var request = HTTPTask() -var opt = request.create("http://vluxe.io", method: .GET, parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let data = response.responseObject as? NSData { - let str = NSString(data: data, encoding: NSUTF8StringEncoding) - println("response: \(str)") //prints the HTML of the page - } - }) -if let o = opt { - operationQueue.addOperation(o) -} +task.cancel() ``` -### Cancel +### JSON Request Serializer -Let's say you want to cancel this request a little later, simple use the operationQueue cancel. +Request parameters can also be serialized to JSON as needed. By default request are serialized using standard HTTP form encoding. ```swift -if let o = opt { - o.cancel() +HTTP.GET("https://google.com", requestSerializer: JSONParameterSerializer()) { response in + //you already get it. The data property of the response object will have the json in it } ``` -### Serializers +### Progress -Request parameters and request responses can also be serialized as needed. By default request are serialized using standard HTTP form encoding. A JSON request and response serializer are provided as well. It is also very simple to create custom serializer by subclass a request or response serializer +SwiftHTTP can monitor the progress of a request. ```swift -var request = HTTPTask() -//The parameters will be encoding as JSON data and sent. -request.requestSerializer = JSONRequestSerializer() -//The expected response will be JSON and be converted to an object return by NSJSONSerialization instead of a NSData. -request.responseSerializer = JSONResponseSerializer() -request.GET("http://vluxe.io", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let dict = response.responseObject as? Dictionary { - let value = dict["key"]! - println("example of the JSON key: \(value)") - println("print the whole response: \(response)") - } - }) +var req = URLRequest(urlString: "https://domain.com/somefile") +let task = HTTP(req!) +task.progress = { progress in + print("progress: \(progress)") //this will be between 0 and 1. +} +task.run { (response) in + //do stuff +} ``` -### UI Changes -All completionHandler closures return on a background thread. This allows any data parsing to be done without blocking the UI. To make update the UI, call `dispatch_async(dispatch_get_main_queue(),{...}`. +### Global handlers + +SwiftHTTP also has global handlers, to reduce the requirement of repeat HTTP modifiers, such as a auth header or setting `NSMutableURLRequest` properties such as `timeoutInterval`. ```swift -var request = HTTPTask() -request.GET("http://vluxe.io", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let data = response.responseObject as? NSData { - let str = NSString(data: data, encoding: NSUTF8StringEncoding) - println("response: \(str)") //prints the HTML of the page - dispatch_async(dispatch_get_main_queue(),{ - self.label.text = str //update the label's text with the HTML content - }) - } -}) -``` +//modify NSMutableURLRequest for any Factory method call (e.g. HTTP.GET, HTTP.POST, HTTP.New, etc). +HTTP.globalRequest { req in + req.timeoutInterval = 5 +} +//set a global SSL pinning setting +HTTP.globalSecurity(HTTPSecurity()) //see the SSL section for more info +//set global auth handler. See the Auth section for more info +HTTP.globalAuth { challenge in + return NSURLCredential(user: "user", password: "passwd", persistence: .ForSession) +} +``` ## Client/Server Example @@ -383,46 +283,96 @@ func main() { Now for the request: ```swift -//The object that will represent our response. More Info in the JSON Parsing section below. -struct Status : JSONJoy { - var status: String? - init() { +struct Response: Codable { + let status: String +} +let decoder = JSONDecoder() +HTTP.GET("http://localhost:8080/bar") { response in + if let error = response.error { + print("got an error: \(error)") + return } - init(_ decoder: JSONDecoder) { - status = decoder["status"].string + do { + let resp = try decoder.decode(Response.self, from: response.data) + print("completed: \(resp.status)") + } catch let error { + print("decode json error: \(error)") } } -//The request -var request = HTTPTask() -request.requestSerializer = HTTPRequestSerializer() -request.requestSerializer.headers["someKey"] = "SomeValue" //example of adding a header value -request.responseSerializer = JSONResponseSerializer() -request.GET("http://localhost:8080/bar", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - println("error: \(err.localizedDescription)") - return //also notify app of failure as needed - } - if let obj: AnyObject = response.responseObject { - let resp = Status(JSONDecoder(obj)) - println("status is: \(resp.status)") - } -}) ``` -## JSON Parsing +## POST example + +```go +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" +) + +func main() { + http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + fmt.Println("header: ", r.Header.Get("Content-Type")) + upload, header, err := r.FormFile("file") + if err != nil { + w.Write([]byte("{\"error\": \"bad file upload\"}")) //normally be a 500 status code + return + } + file, err := os.Create(header.Filename) // we would normally need to generate unique filenames. + if err != nil { + w.Write([]byte("{\"error\": \"system error occured\"}")) //normally be a 500 status code + return + } + io.Copy(file, upload) // write the uploaded file to disk. + w.Write([]byte("{\"status\": \"ok\"}")) + }) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +Now for the Swift: -Swift has a lot of great JSON parsing libraries, but I made one specifically designed for JSON to object serialization. +```swift +struct Response: Codable { + let status: String? + let error: String? +} -[JSONJoy-Swift](https://github.com/daltoniam/JSONJoy-Swift) +let decoder = JSONDecoder() +let url = URL(fileURLWithPath: "/Users/dalton/Desktop/picture.jpg") +HTTP.POST("http://localhost:8080/bar", parameters: ["test": "value", "file": Upload(fileUrl: url)]) { response in + if let error = response.error { + print("got an error: \(error)") + return + } + do { + let resp = try decoder.decode(Response.self, from: response.data) + if let err = resp.error { + print("got an error: \(err)") + } + if let status = resp.status { + print("completed: \(status)") + } + } catch let error { + print("decode json error: \(error)") + } +} +``` ## Requirements -SwiftHTTP works with iOS 7/OSX 10.9 or above. It is recommended to use iOS 8/10.10 or above for Cocoapods/framework support. +SwiftHTTP works with iOS 7/OSX 10.10 or above. It is recommended to use iOS 8/10.10 or above for CocoaPods/framework support. +To use SwiftHTTP with a project targeting iOS 7, you must include all Swift files directly in your project. ## Installation -### Cocoapods +### CocoaPods Check out [Get Started](https://guides.cocoapods.org/using/getting-started.html) tab on [cocoapods.org](http://cocoapods.org/). @@ -432,7 +382,7 @@ To use SwiftHTTP in your project add the following 'Podfile' to your project platform :ios, '8.0' use_frameworks! - pod 'SwiftHTTP', '~> 0.9.4' + pod 'SwiftHTTP', '~> 3.0.1' Then run: @@ -444,11 +394,24 @@ Check out the [Carthage](https://github.com/Carthage/Carthage) docs on how to ad [Carthage Install](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) +You can install Carthage with [Homebrew](http://brew.sh/) using the following command: + +```bash +$ brew update +$ brew install carthage +``` + +To integrate SwiftHTTP into your Xcode project using Carthage, specify it in your `Cartfile`: + +``` +github "daltoniam/SwiftHTTP" >= 3.0.1 +``` + ### Rogue First see the [installation docs](https://github.com/acmacalister/Rogue) for how to install Rogue. -To install SwiftLog run the command below in the directory you created the rogue file. +To install SwiftHTTP run the command below in the directory you created the rogue file. ``` rogue add https://github.com/daltoniam/SwiftHTTP @@ -468,6 +431,7 @@ If you are running this in an OSX app or on a physical iOS device you will need ## TODOs +- [ ] Linux support? - [ ] Add more unit tests ## License diff --git a/Source/HTTPSecurity.swift b/Source/HTTPSecurity.swift new file mode 100644 index 0000000..fb4d9e3 --- /dev/null +++ b/Source/HTTPSecurity.swift @@ -0,0 +1,239 @@ +// +// HTTPSecurity.swift +// SwiftHTTP +// +// Created by Dalton Cherry on 5/16/15. +// Copyright (c) 2015 Vluxe. All rights reserved. +// + +import Foundation +import Security + +open class SSLCert { + var certData: Data? + var key: SecKey? + + /** + Designated init for certificates + + - parameter data: is the binary data of the certificate + + - returns: a representation security object to be used with + */ + public init(data: Data) { + self.certData = data + } + + /** + Designated init for public keys + + - parameter key: is the public key to be used + + - returns: a representation security object to be used with + */ + public init(key: SecKey) { + self.key = key + } +} + +open class HTTPSecurity { + open var validatedDN = true //should the domain name be validated? + + var isReady = false //is the key processing done? + var certificates: [Data]? //the certificates + var pubKeys: [SecKey]? //the public keys + var usePublicKeys = false //use public keys or certificate validation? + + /** + Use certs from main app bundle + + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public convenience init(usePublicKeys: Bool = false) { + let paths = Bundle.main.paths(forResourcesOfType: "cer", inDirectory: ".") + var collect = Array() + for path in paths { + if let d = try? Data(contentsOf: URL(fileURLWithPath: path as String)) { + collect.append(SSLCert(data: d)) + } + } + self.init(certs:collect, usePublicKeys: usePublicKeys) + } + + /** + Designated init + + - parameter keys: is the certificates or public keys to use + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public init(certs: [SSLCert], usePublicKeys: Bool) { + self.usePublicKeys = usePublicKeys + + if self.usePublicKeys { + DispatchQueue.global().async { + var collect = Array() + for cert in certs { + if let data = cert.certData , cert.key == nil { + cert.key = self.extractPublicKey(data) + } + if let k = cert.key { + collect.append(k) + } + } + self.pubKeys = collect + self.isReady = true + } + } else { + var collect = Array() + for cert in certs { + if let d = cert.certData { + collect.append(d) + } + } + self.certificates = collect + self.isReady = true + } + } + + /** + Valid the trust and domain name. + + - parameter trust: is the serverTrust to validate + - parameter domain: is the CN domain to validate + + - returns: if the key was successfully validated + */ + open func isValid(_ trust: SecTrust, domain: String?) -> Bool { + + var tries = 0 + while(!self.isReady) { + usleep(1000) + tries += 1 + if tries > 5 { + return false //doesn't appear it is going to ever be ready... + } + } + var policy: SecPolicy + if self.validatedDN { + policy = SecPolicyCreateSSL(true, domain as CFString?) + } else { + policy = SecPolicyCreateBasicX509() + } + SecTrustSetPolicies(trust,policy) + if self.usePublicKeys { + if let keys = self.pubKeys { + var trustedCount = 0 + let serverPubKeys = publicKeyChainForTrust(trust) + for serverKey in serverPubKeys as [AnyObject] { + for key in keys as [AnyObject] { + if serverKey.isEqual(key) { + trustedCount += 1 + break + } + } + } + if trustedCount == serverPubKeys.count { + return true + } + } + } else if let certs = self.certificates { + let serverCerts = certificateChainForTrust(trust) + var collect = Array() + for cert in certs { + collect.append(SecCertificateCreateWithData(nil,cert as CFData)!) + } + SecTrustSetAnchorCertificates(trust,collect as CFArray) + var result: SecTrustResultType = SecTrustResultType(rawValue: UInt32(0))! + SecTrustEvaluate(trust,&result) + if result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed { + var trustedCount = 0 + for serverCert in serverCerts { + for cert in certs { + if cert == serverCert { + trustedCount += 1 + break + } + } + } + if trustedCount == serverCerts.count { + return true + } + } + } + return false + } + + /** + Get the public key from a certificate data + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + func extractPublicKey(_ data: Data) -> SecKey? { + let possibleCert = SecCertificateCreateWithData(nil,data as CFData) + if let cert = possibleCert { + return extractPublicKeyFromCert(cert, policy: SecPolicyCreateBasicX509()) + } + return nil + } + + /** + Get the public key from a certificate + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + func extractPublicKeyFromCert(_ cert: SecCertificate, policy: SecPolicy) -> SecKey? { + var possibleTrust: SecTrust? + SecTrustCreateWithCertificates(cert, policy, &possibleTrust) + if let trust = possibleTrust { + var result: SecTrustResultType = SecTrustResultType(rawValue: UInt32(0))! + SecTrustEvaluate(trust, &result) + return SecTrustCopyPublicKey(trust) + } + return nil + } + + /** + Get the certificate chain for the trust + + - parameter trust: is the trust to lookup the certificate chain for + + - returns: the certificate chain for the trust + */ + func certificateChainForTrust(_ trust: SecTrust) -> Array { + var collect = Array() + for i in 0 ..< SecTrustGetCertificateCount(trust) { + let cert = SecTrustGetCertificateAtIndex(trust,i) + collect.append(SecCertificateCopyData(cert!) as Data) + } + return collect + } + + /** + Get the public key chain for the trust + + - parameter trust: is the trust to lookup the certificate chain and extract the public keys + + - returns: the public keys from the certifcate chain for the trust + */ + func publicKeyChainForTrust(_ trust: SecTrust) -> Array { + var collect = Array() + let policy = SecPolicyCreateBasicX509() + for i in 0 ..< SecTrustGetCertificateCount(trust) { + let cert = SecTrustGetCertificateAtIndex(trust,i) + if let key = extractPublicKeyFromCert(cert!, policy: policy) { + collect.append(key) + } + } + return collect + } + + +} diff --git a/Info.plist b/Source/Info.plist similarity index 89% rename from Info.plist rename to Source/Info.plist index 79ab576..dc2b99a 100644 --- a/Info.plist +++ b/Source/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.vluxe.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 3.0.1 CFBundleSignature ???? CFBundleVersion diff --git a/Source/Operation.swift b/Source/Operation.swift new file mode 100644 index 0000000..90e2b41 --- /dev/null +++ b/Source/Operation.swift @@ -0,0 +1,556 @@ +// +// Operation.swift +// SwiftHTTP +// +// Created by Dalton Cherry on 8/2/15. +// Copyright © 2015 vluxe. All rights reserved. +// + +import Foundation + +enum HTTPOptError: Error { + case invalidRequest +} + +/** +This protocol exist to allow easy and customizable swapping of a serializing format within an class methods of HTTP. +*/ +public protocol HTTPSerializeProtocol { + + /** + implement this protocol to support serializing parameters to the proper HTTP body or URL + -parameter request: The URLRequest object you will modify to add the parameters to + -parameter parameters: The container (array or dictionary) to convert and append to the URL or Body + */ + func serialize(_ request: inout URLRequest, parameters: HTTPParameterProtocol) -> Error? +} + +/** +Standard HTTP encoding +*/ +public struct HTTPParameterSerializer: HTTPSerializeProtocol { + public init() { } + public func serialize(_ request: inout URLRequest, parameters: HTTPParameterProtocol) -> Error? { + return request.appendParameters(parameters) + } +} + +/** +Send the data as a JSON body +*/ +public struct JSONParameterSerializer: HTTPSerializeProtocol { + public init() { } + public func serialize(_ request: inout URLRequest, parameters: HTTPParameterProtocol) -> Error? { + return request.appendParametersAsJSON(parameters) + } +} + +/** +All the things of an HTTP response +*/ +open class Response { + /// The header values in HTTP response. + open var headers: Dictionary? + /// The mime type of the HTTP response. + open var mimeType: String? + /// The suggested filename for a downloaded file. + open var suggestedFilename: String? + /// The body data of the HTTP response. + open var data: Data { + return collectData as Data + } + /// The status code of the HTTP response. + open var statusCode: Int? + /// The URL of the HTTP response. + open var URL: Foundation.URL? + /// The Error of the HTTP response (if there was one). + open var error: Error? + ///Returns the response as a string + open var text: String? { + return String(data: data, encoding: .utf8) + } + ///get the description of the response + open var description: String { + var buffer = "" + if let u = URL { + buffer += "URL:\n\(u)\n\n" + } + if let code = self.statusCode { + buffer += "Status Code:\n\(code)\n\n" + } + if let heads = headers { + buffer += "Headers:\n" + for (key, value) in heads { + buffer += "\(key): \(value)\n" + } + buffer += "\n" + } + if let t = text { + buffer += "Payload:\n\(t)\n" + } + return buffer + } + ///private things + + ///holds the collected data + var collectData = NSMutableData() + ///finish closure + var completionHandler:((Response) -> Void)? + + //progress closure. Progress is between 0 and 1. + var progressHandler:((Float) -> Void)? + + //download closure. the URL is the file URL where the temp file has been download. + //This closure will be called so you can move the file where you desire. + var downloadHandler:((Response, URL) -> Void)? + + ///This gets called on auth challenges. If nil, default handling is use. + ///Returning nil from this method will cause the request to be rejected and cancelled + var auth:((URLAuthenticationChallenge) -> URLCredential?)? + + ///This is for doing SSL pinning + var security: HTTPSecurity? +} + +/** +The class that does the magic. Is a subclass of NSOperation so you can use it with operation queues or just a good ole HTTP request. +*/ +open class HTTP { + /** + Get notified with a request finishes. + */ + open var onFinish:((Response) -> Void)? { + didSet { + if let handler = onFinish { + DelegateManager.sharedInstance.addTask(task, completionHandler: { (response: Response) in + handler(response) + }) + } + } + } + ///This is for handling authenication + open var auth:((URLAuthenticationChallenge) -> URLCredential?)? { + set { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return } + resp.auth = newValue + } + get { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return nil } + return resp.auth + } + } + + ///This is for doing SSL pinning + open var security: HTTPSecurity? { + set { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return } + resp.security = newValue + } + get { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return nil } + return resp.security + } + } + + ///This is for monitoring progress + open var progress: ((Float) -> Void)? { + set { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return } + resp.progressHandler = newValue + } + get { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return nil } + return resp.progressHandler + } + } + + ///This is for handling downloads + open var downloadHandler: ((Response, URL) -> Void)? { + set { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return } + resp.downloadHandler = newValue + } + get { + guard let resp = DelegateManager.sharedInstance.responseForTask(task) else { return nil } + return resp.downloadHandler + } + } + + ///the actual task + var task: URLSessionTask! + + /** + creates a new HTTP request. + */ + public init(_ req: URLRequest, session: URLSession = SharedSession.defaultSession, isDownload: Bool = false) { + if isDownload { + task = session.downloadTask(with: req) + } else { + task = session.dataTask(with: req) + } + DelegateManager.sharedInstance.addResponseForTask(task) + } + + /** + start/sends the HTTP task with a completionHandler. Use this when *NOT* using an NSOperationQueue. + */ + open func run(_ completionHandler: ((Response) -> Void)? = nil) { + if let handler = completionHandler { + onFinish = handler + } + task.resume() + } + + /** + Cancel the running task + */ + open func cancel() { + task.cancel() + } + + /** + Class method to run a GET request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func GET(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, + requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .GET, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + /** + Class method to run a HEAD request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func HEAD(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .HEAD, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + /** + Class method to run a DELETE request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func DELETE(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .DELETE, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + /** + Class method to run a POST request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func POST(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .POST, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + /** + Class method to run a PUT request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func PUT(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, + requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .PUT, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + /** + Class method to run a PUT request that handles the URLRequest and parameter encoding for you. + */ + @discardableResult open class func PATCH(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + return Run(url, method: .PATCH, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) + } + + @discardableResult class func Run(_ url: String, method: HTTPVerb, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + guard let task = HTTP.New(url, method: method, parameters: parameters, headers: headers, requestSerializer: requestSerializer, completionHandler: completionHandler) else {return nil} + task.run() + return task + } + + /** + Class method to create a Download request that handles the URLRequest and parameter encoding for you. + */ + open class func Download(_ url: String, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, + requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completion:@escaping ((Response, URL) -> Void)) { + guard let task = HTTP.New(url, method: .GET, parameters: parameters, headers: headers, requestSerializer: requestSerializer) else {return} + task.downloadHandler = completion + task.run() + } + + /** + Class method to create a HTTP request that handles the URLRequest and parameter encoding for you. + */ + open class func New(_ url: String, method: HTTPVerb, parameters: HTTPParameterProtocol? = nil, headers: [String:String]? = nil, requestSerializer: HTTPSerializeProtocol = HTTPParameterSerializer(), completionHandler: ((Response) -> Void)? = nil) -> HTTP? { + guard var req = URLRequest(urlString: url, headers: headers) else { + guard let handler = completionHandler else { return nil } + let resp = Response() + resp.error = HTTPOptError.invalidRequest + handler(resp) + return nil + } + if let handler = DelegateManager.sharedInstance.requestHandler { + handler(&req) + } + req.verb = method + if let params = parameters { + if let error = requestSerializer.serialize(&req, parameters: params) { + guard let handler = completionHandler else { return nil } + let resp = Response() + resp.error = error + handler(resp) + return nil + } + } + let httpReq = HTTP(req) + httpReq.onFinish = completionHandler + return httpReq + } + + /** + Set the global auth handler + */ + open class func globalAuth(_ handler: ((URLAuthenticationChallenge) -> URLCredential?)?) { + DelegateManager.sharedInstance.auth = handler + } + + /** + Set the global security handler + */ + open class func globalSecurity(_ security: HTTPSecurity?) { + DelegateManager.sharedInstance.security = security + } + + /** + Set the global request handler + */ + open class func globalRequest(_ handler: ((inout URLRequest) -> Void)?) { + DelegateManager.sharedInstance.requestHandler = handler + } +} + +extension HTTP { + static func == (left: HTTP, right: HTTP) -> Bool { + return left.task.taskIdentifier == right.task.taskIdentifier + } + + static func != (left: HTTP, right: HTTP) -> Bool { + return !(left == right) + } +} + +/** +Absorb all the delegates methods of NSURLSession and forwards them to pretty closures. +This is basically the sin eater for NSURLSession. +*/ +public class DelegateManager: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate { + //the singleton to handle delegate needs of NSURLSession + static let sharedInstance = DelegateManager() + + /// this is for global authenication handling + var auth:((URLAuthenticationChallenge) -> URLCredential?)? + + ///This is for global SSL pinning + var security: HTTPSecurity? + + /// this is for global request handling + var requestHandler:((inout URLRequest) -> Void)? + + var taskMap = Dictionary() + //"install" a task by adding the task to the map and setting the completion handler + func addTask(_ task: URLSessionTask, completionHandler:@escaping ((Response) -> Void)) { + addResponseForTask(task) + if let resp = responseForTask(task) { + resp.completionHandler = completionHandler + } + } + + //"remove" a task by removing the task from the map + func removeTask(_ task: URLSessionTask) { + taskMap.removeValue(forKey: task.taskIdentifier) + } + + //add the response task + func addResponseForTask(_ task: URLSessionTask) { + if taskMap[task.taskIdentifier] == nil { + taskMap[task.taskIdentifier] = Response() + } + } + //get the response object for the task + func responseForTask(_ task: URLSessionTask) -> Response? { + return taskMap[task.taskIdentifier] + } + + //handle getting data + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + addResponseForTask(dataTask) + guard let resp = responseForTask(dataTask) else { return } + resp.collectData.append(data) + if resp.progressHandler != nil { //don't want the extra cycles for no reason + guard let taskResp = dataTask.response else { return } + progressHandler(resp, expectedLength: taskResp.expectedContentLength, currentLength: Int64(resp.collectData.length)) + } + } + + //handle task finishing + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let resp = responseForTask(task) else { return } + resp.error = error as NSError? + if let hresponse = task.response as? HTTPURLResponse { + resp.headers = hresponse.allHeaderFields as? Dictionary + resp.mimeType = hresponse.mimeType + resp.suggestedFilename = hresponse.suggestedFilename + resp.statusCode = hresponse.statusCode + resp.URL = hresponse.url + } + if let code = resp.statusCode, code > 299 { + resp.error = createError(code) + } + if let handler = resp.completionHandler { + handler(resp) + } + removeTask(task) + } + + //handle authenication + public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + var sec = security + var au = auth + if let resp = responseForTask(task) { + if let s = resp.security { + sec = s + } + if let a = resp.auth { + au = a + } + } + if let sec = sec , challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + let space = challenge.protectionSpace + if let trust = space.serverTrust { + if sec.isValid(trust, domain: space.host) { + completionHandler(.useCredential, URLCredential(trust: trust)) + return + } + } + completionHandler(.cancelAuthenticationChallenge, nil) + return + + } else if let a = au { + let cred = a(challenge) + if let c = cred { + completionHandler(.useCredential, c) + return + } + completionHandler(.rejectProtectionSpace, nil) + return + } + completionHandler(.performDefaultHandling, nil) + } + + //upload progress + public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + guard let resp = responseForTask(task) else { return } + progressHandler(resp, expectedLength: totalBytesExpectedToSend, currentLength: totalBytesSent) + } + + //download progress + public func urlSession(_ session: Foundation.URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + guard let resp = responseForTask(downloadTask) else { return } + progressHandler(resp, expectedLength: totalBytesExpectedToWrite, currentLength: totalBytesWritten) + } + + //handle download task + public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + guard let resp = responseForTask(downloadTask) else { return } + guard let handler = resp.downloadHandler else { return } + handler(resp, location) + } + + //handle progress + public func progressHandler(_ response: Response, expectedLength: Int64, currentLength: Int64) { + guard let handler = response.progressHandler else { return } + let slice = Float(1.0)/Float(expectedLength) + handler(slice*Float(currentLength)) + } + + /** + Create an error for response you probably don't want (400-500 HTTP responses for example). + + -parameter code: Code for error. + + -returns An NSError. + */ + fileprivate func createError(_ code: Int) -> NSError { + let text = HTTPStatusCode(statusCode: code).statusDescription + return NSError(domain: "HTTP", code: code, userInfo: [NSLocalizedDescriptionKey: text]) + } +} + +/** +Handles providing singletons of NSURLSession. +*/ +public class SharedSession { + public static let defaultSession = URLSession(configuration: URLSessionConfiguration.default, + delegate: DelegateManager.sharedInstance, delegateQueue: nil) + static let ephemeralSession = URLSession(configuration: URLSessionConfiguration.ephemeral, + delegate: DelegateManager.sharedInstance, delegateQueue: nil) +} + + +/** + Bare bones queue to manage HTTP Requests + */ +open class HTTPQueue { + public var maxSimultaneousRequest = 5 + var queue = [HTTP]() + let mutex = NSLock() + var activeReq = [Int: HTTP]() + var finishedHandler: (() -> Void)? + + public init(maxSimultaneousRequest: Int) { + self.maxSimultaneousRequest = maxSimultaneousRequest + } + + open func add(request: URLRequest) { + add(http: HTTP(request)) + } + + open func add(http: HTTP) { + var doWork = false + mutex.lock() + queue.append(http) + if activeReq.count < maxSimultaneousRequest { + doWork = true + } + mutex.unlock() + if doWork { + run() + } + } + + open func finished(queue: DispatchQueue = DispatchQueue.main, completionHandler: @escaping (() -> Void)) { + finishedHandler = completionHandler + } + + func run() { + guard let http = nextItem() else { + mutex.lock() + let count = activeReq.count + mutex.unlock() + if count == 0 { + finishedHandler?() + } + return + } + let handler = http.onFinish + http.run {[weak self] (response) in + handler?(response) + self?.mutex.lock() + self?.activeReq.removeValue(forKey: http.task.taskIdentifier) + self?.mutex.unlock() + self?.run() + } + } + + func nextItem() -> HTTP? { + mutex.lock() + if queue.count == 0 { + mutex.unlock() + return nil + } + let next = queue.removeFirst() + activeReq[next.task.taskIdentifier] = next + mutex.unlock() + return next + } +} diff --git a/Source/Request.swift b/Source/Request.swift new file mode 100644 index 0000000..6767352 --- /dev/null +++ b/Source/Request.swift @@ -0,0 +1,361 @@ +// +// Request.swift +// SwiftHTTP +// +// Created by Dalton Cherry on 8/16/15. +// Copyright © 2015 vluxe. All rights reserved. +// + +import Foundation + + +extension String { + /** + A simple extension to the String object to encode it for web request. + + :returns: Encoded version of of string it was called as. + */ + var escaped: String? { + var set = CharacterSet() + set.formUnion(CharacterSet.urlQueryAllowed) + set.remove(charactersIn: "[].:/?&=;+!@#$()',*\"") // remove the HTTP ones from the set. + return self.addingPercentEncoding(withAllowedCharacters: set) + } + + /** + A simple extension to the String object to url encode quotes only. + + :returns: string with . + */ + var quoteEscaped: String { + return self.replacingOccurrences(of: "\"", with: "%22").replacingOccurrences(of: "'", with: "%27") + } +} + +/** +The standard HTTP Verbs +*/ +public enum HTTPVerb: String { + case GET = "GET" + case POST = "POST" + case PUT = "PUT" + case HEAD = "HEAD" + case DELETE = "DELETE" + case PATCH = "PATCH" + case OPTIONS = "OPTIONS" + case TRACE = "TRACE" + case CONNECT = "CONNECT" + case UNKNOWN = "UNKNOWN" +} + +/** +This is used to create key/value pairs of the parameters +*/ +public struct HTTPPair { + var key: String? + let storeVal: AnyObject + /** + Create the object with a possible key and a value + */ + init(key: String?, value: AnyObject) { + self.key = key + self.storeVal = value + } + /** + Computed property of the string representation of the storedVal + */ + var upload: Upload? { + return storeVal as? Upload + } + /** + Computed property of the string representation of the storedVal + */ + var value: String { + if storeVal is NSNull { + return "" + } else if let v = storeVal as? String { + return v + } else { + return storeVal.description ?? "" + } + } + /** + Computed property of the string representation of the storedVal escaped for URLs + */ + var escapedValue: String { + let v = value.escaped ?? "" + if let k = key { + if let escapedKey = k.escaped { + return "\(escapedKey)=\(v)" + } + } + return "" + } +} + +/** + This is super gross, but it is just an edge case, I'm willing to live with it + versus trying to handle such an rare need with more code and confusion + */ +public class HTTPParameterProtocolSettings { + public static var sendEmptyArray = false +} + +/** +This protocol is used to make the dictionary and array serializable into key/value pairs. +*/ +public protocol HTTPParameterProtocol { + func createPairs(_ key: String?) -> [HTTPPair] +} + +/** +Support for the Dictionary type as an HTTPParameter. +*/ +extension Dictionary: HTTPParameterProtocol { + public func createPairs(_ key: String?) -> [HTTPPair] { + var collect = [HTTPPair]() + for (k, v) in self { + if let nestedKey = k as? String { + let useKey = key != nil ? "\(key!)[\(nestedKey)]" : nestedKey + if let subParam = v as? HTTPParameterProtocol { + collect.append(contentsOf: subParam.createPairs(useKey)) + } else { + collect.append(HTTPPair(key: useKey, value: v as AnyObject)) + } + } + } + return collect + } +} + +/** +Support for the Array type as an HTTPParameter. +*/ +extension Array: HTTPParameterProtocol { + + public func createPairs(_ key: String?) -> [HTTPPair] { + var collect = [HTTPPair]() + for v in self { + let useKey = key != nil ? "\(key!)[]" : key + if let subParam = v as? HTTPParameterProtocol { + collect.append(contentsOf: subParam.createPairs(useKey)) + } else { + collect.append(HTTPPair(key: useKey, value: v as AnyObject)) + } + } + if HTTPParameterProtocolSettings.sendEmptyArray && collect.count == 0 { + collect.append(HTTPPair(key: key, value: "[]" as AnyObject)) + } + return collect + } +} + +/** +Support for the Upload type as an HTTPParameter. +*/ +extension Upload: HTTPParameterProtocol { + public func createPairs(_ key: String?) -> Array { + var collect = Array() + collect.append(HTTPPair(key: key, value: self)) + return collect + } +} + +/** +Adds convenience methods to URLRequest to make using it with HTTP much simpler. +*/ +extension URLRequest { + /** + Convenience init to allow init with a string. + -parameter urlString: The string representation of a URL to init with. + */ + public init?(urlString: String, parameters: HTTPParameterProtocol? = nil, headers: [String: String]? = nil, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, timeoutInterval: TimeInterval = 60) { + if let url = URL(string: urlString) { + self.init(url: url) + } else { + return nil + } + if let params = parameters { + let _ = appendParameters(params) + } + if let heads = headers { + for (key,value) in heads { + addValue(value, forHTTPHeaderField: key) + } + } + } + + /** + Convenience method to avoid having to use strings and allow using an enum + */ + public var verb: HTTPVerb { + set { + httpMethod = newValue.rawValue + } + get { + if let verb = httpMethod, let v = HTTPVerb(rawValue: verb) { + return v + } + return .UNKNOWN + } + } + + /** + Used to update the content type in the HTTP header as needed + */ + var contentTypeKey: String { + return "Content-Type" + } + + /** + append the parameters using the standard HTTP Query model. + This is parameters in the query string of the url (e.g. ?first=one&second=two for GET, HEAD, DELETE. + It uses 'application/x-www-form-urlencoded' for the content type of POST/PUT requests that don't contains files. + If it contains a file it uses `multipart/form-data` for the content type. + -parameter parameters: The container (array or dictionary) to convert and append to the URL or Body + */ + public mutating func appendParameters(_ parameters: HTTPParameterProtocol) -> Error? { + if isURIParam() { + appendParametersAsQueryString(parameters) + } else if containsFile(parameters) { + return appendParametersAsMultiPartFormData(parameters) + } else { + appendParametersAsUrlEncoding(parameters) + } + return nil + } + + /** + append the parameters as a HTTP Query string. (e.g. domain.com?first=one&second=two) + -parameter parameters: The container (array or dictionary) to convert and append to the URL + */ + public mutating func appendParametersAsQueryString(_ parameters: HTTPParameterProtocol) { + let queryString = parameters.createPairs(nil).map({ (pair) in + return pair.escapedValue + }).joined(separator: "&") + if let u = self.url , queryString.count > 0 { + let para = u.query != nil ? "&" : "?" + self.url = URL(string: "\(u.absoluteString)\(para)\(queryString)") + } + } + + /** + append the parameters as a url encoded string. (e.g. in the body of the request as: first=one&second=two) + -parameter parameters: The container (array or dictionary) to convert and append to the HTTP body + */ + public mutating func appendParametersAsUrlEncoding(_ parameters: HTTPParameterProtocol) { + if value(forHTTPHeaderField: contentTypeKey) == nil { + var contentStr = "application/x-www-form-urlencoded" + if let charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(String.Encoding.utf8.rawValue)) { + contentStr += "; charset=\(charset)" + } + setValue(contentStr, forHTTPHeaderField:contentTypeKey) + } + let queryString = parameters.createPairs(nil).map({ (pair) in + return pair.escapedValue + }).joined(separator: "&") + httpBody = queryString.data(using: .utf8) + } + + /** + append the parameters as a multpart form body. This is the type normally used for file uploads. + -parameter parameters: The container (array or dictionary) to convert and append to the HTTP body + */ + public mutating func appendParametersAsMultiPartFormData(_ parameters: HTTPParameterProtocol) -> Error? { + let boundary = "Boundary+\(arc4random())\(arc4random())" + if value(forHTTPHeaderField: contentTypeKey) == nil { + setValue("multipart/form-data; boundary=\(boundary)", + forHTTPHeaderField:contentTypeKey) + } + let mutData = NSMutableData() + let multiCRLF = "\r\n" + mutData.append("--\(boundary)".data(using: .utf8)!) + for pair in parameters.createPairs(nil) { + guard let key = pair.key else { continue } //this won't happen, but just to properly unwrap + if let upload = pair.upload { + let resp = upload.getData() + if let error = resp.error { + return error + } + mutData.append("\(multiCRLF)".data(using: .utf8)!) + if let data = resp.data { + mutData.append(multiFormHeader(key, fileName: upload.fileName, + type: upload.mimeType, multiCRLF: multiCRLF).data(using: .utf8)!) + mutData.append(data) + } else { + return HTTPUploadError.noData + } + } else { + mutData.append("\(multiCRLF)".data(using: .utf8)!) + let str = "\(multiFormHeader(key, fileName: nil, type: nil, multiCRLF: multiCRLF))\(pair.value)" + mutData.append(str.data(using: .utf8)!) + } + mutData.append("\(multiCRLF)--\(boundary)".data(using: .utf8)!) + } + mutData.append("--\(multiCRLF)".data(using: .utf8)!) + httpBody = mutData as Data + return nil + } + + /** + Helper method to create the multipart form data + */ + func multiFormHeader(_ name: String, fileName: String?, type: String?, multiCRLF: String) -> String { + var str = "Content-Disposition: form-data; name=\"\(name.quoteEscaped)\"" + if let n = fileName { + str += "; filename=\"\(n.quoteEscaped)\"" + } + str += multiCRLF + if let t = type { + str += "Content-Type: \(t)\(multiCRLF)" + } + str += multiCRLF + return str + } + + + /** + send the parameters as a body of JSON + -parameter parameters: The container (array or dictionary) to convert and append to the URL or Body + */ + public mutating func appendParametersAsJSON(_ parameters: HTTPParameterProtocol) -> Error? { + if isURIParam() { + appendParametersAsQueryString(parameters) + } else { + do { + httpBody = try JSONSerialization.data(withJSONObject: parameters as AnyObject, options: JSONSerialization.WritingOptions()) + } catch let error { + return error + } + var contentStr = "application/json" + if let charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(String.Encoding.utf8.rawValue)) { + contentStr += "; charset=\(charset)" + } + setValue(contentStr, forHTTPHeaderField: contentTypeKey) + } + return nil + } + + /** + Check if the request requires the parameters to be appended to the URL + */ + public func isURIParam() -> Bool { + if verb == .GET || verb == .HEAD || verb == .DELETE { + return true + } + return false + } + + /** + check if the parameters contain a file object within them + -parameter parameters: The parameters to search through for an upload object + */ + public func containsFile(_ parameters: HTTPParameterProtocol) -> Bool { + for pair in parameters.createPairs(nil) { + if let _ = pair.upload { + return true + } + } + return false + } +} diff --git a/Source/StatusCode.swift b/Source/StatusCode.swift new file mode 100644 index 0000000..71bdf93 --- /dev/null +++ b/Source/StatusCode.swift @@ -0,0 +1,159 @@ +// +// HTTPStatusCode.swift +// SwiftHTTP +// +// Created by Yu Kadowaki on 7/12/15. +// Copyright (c) 2015 Vluxe. All rights reserved. +// + +import Foundation + +/// HTTP Status Code (RFC 2616) +public enum HTTPStatusCode: Int { + case `continue` = 100, + switchingProtocols = 101 + + case ok = 200, + created = 201, + accepted = 202, + nonAuthoritativeInformation = 203, + noContent = 204, + resetContent = 205, + partialContent = 206 + + case multipleChoices = 300, + movedPermanently = 301, + found = 302, + seeOther = 303, + notModified = 304, + useProxy = 305, + unused = 306, + temporaryRedirect = 307 + + case badRequest = 400, + unauthorized = 401, + paymentRequired = 402, + forbidden = 403, + notFound = 404, + methodNotAllowed = 405, + notAcceptable = 406, + proxyAuthenticationRequired = 407, + requestTimeout = 408, + conflict = 409, + gone = 410, + lengthRequired = 411, + preconditionFailed = 412, + requestEntityTooLarge = 413, + requestUriTooLong = 414, + unsupportedMediaType = 415, + requestedRangeNotSatisfiable = 416, + expectationFailed = 417 + + case internalServerError = 500, + notImplemented = 501, + badGateway = 502, + serviceUnavailable = 503, + gatewayTimeout = 504, + httpVersionNotSupported = 505 + + case invalidUrl = -1001 + + case unknownStatus = 0 + + init(statusCode: Int) { + self = HTTPStatusCode(rawValue: statusCode) ?? .unknownStatus + } + + public var statusDescription: String { + get { + switch self { + case .continue: + return "Continue" + case .switchingProtocols: + return "Switching protocols" + case .ok: + return "OK" + case .created: + return "Created" + case .accepted: + return "Accepted" + case .nonAuthoritativeInformation: + return "Non authoritative information" + case .noContent: + return "No content" + case .resetContent: + return "Reset content" + case .partialContent: + return "Partial Content" + case .multipleChoices: + return "Multiple choices" + case .movedPermanently: + return "Moved Permanently" + case .found: + return "Found" + case .seeOther: + return "See other Uri" + case .notModified: + return "Not modified" + case .useProxy: + return "Use proxy" + case .unused: + return "Unused" + case .temporaryRedirect: + return "Temporary redirect" + case .badRequest: + return "Bad request" + case .unauthorized: + return "Access denied" + case .paymentRequired: + return "Payment required" + case .forbidden: + return "Forbidden" + case .notFound: + return "Page not found" + case .methodNotAllowed: + return "Method not allowed" + case .notAcceptable: + return "Not acceptable" + case .proxyAuthenticationRequired: + return "Proxy authentication required" + case .requestTimeout: + return "Request timeout" + case .conflict: + return "Conflict request" + case .gone: + return "Page is gone" + case .lengthRequired: + return "Lack content length" + case .preconditionFailed: + return "Precondition failed" + case .requestEntityTooLarge: + return "Request entity is too large" + case .requestUriTooLong: + return "Request uri is too long" + case .unsupportedMediaType: + return "Unsupported media type" + case .requestedRangeNotSatisfiable: + return "Request range is not satisfiable" + case .expectationFailed: + return "Expected request is failed" + case .internalServerError: + return "Internal server error" + case .notImplemented: + return "Server does not implement a feature for request" + case .badGateway: + return "Bad gateway" + case .serviceUnavailable: + return "Service unavailable" + case .gatewayTimeout: + return "Gateway timeout" + case .httpVersionNotSupported: + return "Http version not supported" + case .invalidUrl: + return "Invalid url" + default: + return "Unknown status code" + } + } + } +} diff --git a/SwiftHTTP.h b/Source/SwiftHTTP.h similarity index 100% rename from SwiftHTTP.h rename to Source/SwiftHTTP.h diff --git a/Source/Upload.swift b/Source/Upload.swift new file mode 100644 index 0000000..160c668 --- /dev/null +++ b/Source/Upload.swift @@ -0,0 +1,117 @@ +// +// Upload.swift +// SwiftHTTP +// +// Created by Dalton Cherry on 6/5/14. +// Copyright (c) 2014 Vluxe. All rights reserved. +// + +import Foundation + +#if os(iOS) + import MobileCoreServices +#endif + +/** + Upload errors + */ +enum HTTPUploadError: Error { + case noFileUrl + case noData +} + +/** +This is how to upload files in SwiftHTTP. The upload object represents a file to upload by either a data blob or a url (which it reads off disk). +*/ +open class Upload: NSObject, NSCoding { + var fileUrl: URL? { + didSet { + getMimeType() + } + } + var mimeType: String? + var data: Data? + var fileName: String? + + /** + Tries to determine the mime type from the fileUrl extension. + */ + func getMimeType() { + mimeType = "application/octet-stream" + guard let url = fileUrl else { return } + #if os(iOS) || os(OSX) //for watchOS support + guard let UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, url.pathExtension as CFString, nil) else { return } + guard let str = UTTypeCopyPreferredTagWithClass(UTI.takeRetainedValue(), kUTTagClassMIMEType) else { return } + mimeType = str.takeRetainedValue() as String + #endif + } + + /** + Reads the data from disk or from memory. Throws an error if no data or file is found. + */ + open func getData() -> (data: Data?, error: Error?) { + if let d = data { + return (d, nil) + } + guard let url = fileUrl else { return (nil, HTTPUploadError.noFileUrl) } + fileName = url.lastPathComponent + do { + let d = try Data(contentsOf: url, options: .mappedIfSafe) + data = d + getMimeType() + return (d, nil) + } catch let error { + return (nil, error) + } + } + + /** + Standard NSCoder support + */ + open func encode(with aCoder: NSCoder) { + aCoder.encode(self.fileUrl, forKey: "fileUrl") + aCoder.encode(self.mimeType, forKey: "mimeType") + aCoder.encode(self.fileName, forKey: "fileName") + aCoder.encode(self.data, forKey: "data") + } + + /** + Required for NSObject support (because of NSCoder, it would be a struct otherwise!) + */ + public override init() { + super.init() + } + + required public convenience init(coder aDecoder: NSCoder) { + self.init() + fileUrl = aDecoder.decodeObject(forKey: "fileUrl") as? URL + mimeType = aDecoder.decodeObject(forKey: "mimeType") as? String + fileName = aDecoder.decodeObject(forKey: "fileName") as? String + data = aDecoder.decodeObject(forKey: "data") as? Data + } + + /** + Initializes a new Upload object with a fileUrl. The fileName and mimeType will be infered. + + -parameter fileUrl: The fileUrl is a standard url path to a file. + */ + public convenience init(fileUrl: URL) { + self.init() + self.fileUrl = fileUrl + } + + /** + Initializes a new Upload object with a data blob. + + -parameter data: The data is a NSData representation of a file's data. + -parameter fileName: The fileName is just that. The file's name. + -parameter mimeType: The mimeType is just that. The mime type you would like the file to uploaded as. + */ + ///upload a file from a a data blob. Must add a filename and mimeType as that can't be infered from the data + public convenience init(data: Data, fileName: String, mimeType: String) { + self.init() + self.data = data + self.fileName = fileName + self.mimeType = mimeType + } +} diff --git a/SwiftHTTP.podspec b/SwiftHTTP.podspec index 115fa5b..3707f9f 100644 --- a/SwiftHTTP.podspec +++ b/SwiftHTTP.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SwiftHTTP" - s.version = "0.9.5" + s.version = "3.0.1" s.summary = "Thin wrapper around NSURLSession in Swift. Simplifies HTTP requests." s.homepage = "https://github.com/daltoniam/SwiftHTTP" s.license = 'Apache License, Version 2.0' @@ -8,7 +8,8 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/daltoniam/SwiftHTTP.git', :tag => "#{s.version}"} s.social_media_url = 'http://twitter.com/daltoniam' s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.9' - s.source_files = '*.swift' - s.requires_arc = 'true' + s.osx.deployment_target = '10.10' + s.watchos.deployment_target = '2.0' + s.tvos.deployment_target = '9.0' + s.source_files = 'Source/*.swift' end diff --git a/SwiftHTTP.xcodeproj/project.pbxproj b/SwiftHTTP.xcodeproj/project.pbxproj index 084cdac..4a85c17 100644 --- a/SwiftHTTP.xcodeproj/project.pbxproj +++ b/SwiftHTTP.xcodeproj/project.pbxproj @@ -7,21 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 5CB3204E1B03F5C50050968C /* HTTPSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB3204D1B03F5C50050968C /* HTTPSecurity.swift */; }; - 5CB3204F1B03F5C50050968C /* HTTPSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB3204D1B03F5C50050968C /* HTTPSecurity.swift */; }; - 6BFD903719C8D95C00DD99B6 /* HTTPUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903219C8D95C00DD99B6 /* HTTPUpload.swift */; }; - 6BFD903819C8D95C00DD99B6 /* SwiftHTTP.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BFD903319C8D95C00DD99B6 /* SwiftHTTP.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5C135F161C47366700AA3A01 /* HTTPSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135F101C47366700AA3A01 /* HTTPSecurity.swift */; }; + 5C135F191C47366700AA3A01 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135F111C47366700AA3A01 /* Operation.swift */; }; + 5C135F1C1C47366700AA3A01 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135F121C47366700AA3A01 /* Request.swift */; }; + 5C135F1F1C47366700AA3A01 /* StatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135F131C47366700AA3A01 /* StatusCode.swift */; }; + 5C135F221C47366700AA3A01 /* SwiftHTTP.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C135F141C47366700AA3A01 /* SwiftHTTP.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5C135F251C47366700AA3A01 /* Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135F151C47366700AA3A01 /* Upload.swift */; }; 6BFD904019C8D9A000DD99B6 /* SwiftHTTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903D19C8D98000DD99B6 /* SwiftHTTPTests.swift */; }; - 6BFD904319C928D900DD99B6 /* HTTPRequestSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD902F19C8D95C00DD99B6 /* HTTPRequestSerializer.swift */; }; - 6BFD904419C928D900DD99B6 /* HTTPResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903019C8D95C00DD99B6 /* HTTPResponseSerializer.swift */; }; - 6BFD904519C928D900DD99B6 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903119C8D95C00DD99B6 /* HTTPTask.swift */; }; - D958026419E6EEEB003C8218 /* SwiftHTTP.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D958025919E6EEEB003C8218 /* SwiftHTTP.framework */; }; - D958027219E6EF2B003C8218 /* SwiftHTTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903D19C8D98000DD99B6 /* SwiftHTTPTests.swift */; }; - D958027319E6EF4B003C8218 /* HTTPRequestSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD902F19C8D95C00DD99B6 /* HTTPRequestSerializer.swift */; }; - D958027419E6EF4B003C8218 /* HTTPResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903019C8D95C00DD99B6 /* HTTPResponseSerializer.swift */; }; - D958027519E6EF4B003C8218 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903119C8D95C00DD99B6 /* HTTPTask.swift */; }; - D958027619E6EF4B003C8218 /* HTTPUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFD903219C8D95C00DD99B6 /* HTTPUpload.swift */; }; - D958027719E6EF89003C8218 /* SwiftHTTP.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BFD903319C8D95C00DD99B6 /* SwiftHTTP.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -32,29 +24,20 @@ remoteGlobalIDString = 6BFD901519C8D8B500DD99B6; remoteInfo = SwiftHTTP; }; - D958026519E6EEEB003C8218 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6BFD900D19C8D8B500DD99B6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D958025819E6EEEB003C8218; - remoteInfo = SwiftHTTPOSX; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 5CB3204D1B03F5C50050968C /* HTTPSecurity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPSecurity.swift; sourceTree = SOURCE_ROOT; }; + 5C135F101C47366700AA3A01 /* HTTPSecurity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPSecurity.swift; path = Source/HTTPSecurity.swift; sourceTree = SOURCE_ROOT; }; + 5C135F111C47366700AA3A01 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Operation.swift; path = Source/Operation.swift; sourceTree = SOURCE_ROOT; }; + 5C135F121C47366700AA3A01 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Source/Request.swift; sourceTree = SOURCE_ROOT; }; + 5C135F131C47366700AA3A01 /* StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StatusCode.swift; path = Source/StatusCode.swift; sourceTree = SOURCE_ROOT; }; + 5C135F141C47366700AA3A01 /* SwiftHTTP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SwiftHTTP.h; path = Source/SwiftHTTP.h; sourceTree = SOURCE_ROOT; }; + 5C135F151C47366700AA3A01 /* Upload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Upload.swift; path = Source/Upload.swift; sourceTree = SOURCE_ROOT; }; + 5C135F281C47367400AA3A01 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Source/Info.plist; sourceTree = SOURCE_ROOT; }; 6BFD901619C8D8B500DD99B6 /* SwiftHTTP.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftHTTP.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6BFD902119C8D8B500DD99B6 /* SwiftHTTPTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftHTTPTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 6BFD902F19C8D95C00DD99B6 /* HTTPRequestSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPRequestSerializer.swift; sourceTree = SOURCE_ROOT; }; - 6BFD903019C8D95C00DD99B6 /* HTTPResponseSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HTTPResponseSerializer.swift; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 6BFD903119C8D95C00DD99B6 /* HTTPTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HTTPTask.swift; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 6BFD903219C8D95C00DD99B6 /* HTTPUpload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUpload.swift; sourceTree = SOURCE_ROOT; }; - 6BFD903319C8D95C00DD99B6 /* SwiftHTTP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftHTTP.h; sourceTree = SOURCE_ROOT; }; - 6BFD903919C8D96500DD99B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 6BFD903C19C8D98000DD99B6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = ""; }; 6BFD903D19C8D98000DD99B6 /* SwiftHTTPTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftHTTPTests.swift; path = Tests/SwiftHTTPTests.swift; sourceTree = ""; }; - D958025919E6EEEB003C8218 /* SwiftHTTP.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftHTTP.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D958026319E6EEEB003C8218 /* SwiftHTTPOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftHTTPOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,21 +55,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D958025519E6EEEB003C8218 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D958026019E6EEEB003C8218 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D958026419E6EEEB003C8218 /* SwiftHTTP.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -104,8 +72,6 @@ children = ( 6BFD901619C8D8B500DD99B6 /* SwiftHTTP.framework */, 6BFD902119C8D8B500DD99B6 /* SwiftHTTPTests.xctest */, - D958025919E6EEEB003C8218 /* SwiftHTTP.framework */, - D958026319E6EEEB003C8218 /* SwiftHTTPOSXTests.xctest */, ); name = Products; sourceTree = ""; @@ -113,12 +79,12 @@ 6BFD901819C8D8B500DD99B6 /* SwiftHTTP */ = { isa = PBXGroup; children = ( - 6BFD902F19C8D95C00DD99B6 /* HTTPRequestSerializer.swift */, - 6BFD903019C8D95C00DD99B6 /* HTTPResponseSerializer.swift */, - 6BFD903119C8D95C00DD99B6 /* HTTPTask.swift */, - 6BFD903219C8D95C00DD99B6 /* HTTPUpload.swift */, - 5CB3204D1B03F5C50050968C /* HTTPSecurity.swift */, - 6BFD903319C8D95C00DD99B6 /* SwiftHTTP.h */, + 5C135F101C47366700AA3A01 /* HTTPSecurity.swift */, + 5C135F111C47366700AA3A01 /* Operation.swift */, + 5C135F121C47366700AA3A01 /* Request.swift */, + 5C135F131C47366700AA3A01 /* StatusCode.swift */, + 5C135F151C47366700AA3A01 /* Upload.swift */, + 5C135F141C47366700AA3A01 /* SwiftHTTP.h */, 6BFD901919C8D8B500DD99B6 /* Supporting Files */, ); path = SwiftHTTP; @@ -127,7 +93,7 @@ 6BFD901919C8D8B500DD99B6 /* Supporting Files */ = { isa = PBXGroup; children = ( - 6BFD903919C8D96500DD99B6 /* Info.plist */, + 5C135F281C47367400AA3A01 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; @@ -148,15 +114,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 6BFD903819C8D95C00DD99B6 /* SwiftHTTP.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D958025619E6EEEB003C8218 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - D958027719E6EF89003C8218 /* SwiftHTTP.h in Headers */, + 5C135F221C47366700AA3A01 /* SwiftHTTP.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -199,63 +157,24 @@ productReference = 6BFD902119C8D8B500DD99B6 /* SwiftHTTPTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - D958025819E6EEEB003C8218 /* SwiftHTTPOSX */ = { - isa = PBXNativeTarget; - buildConfigurationList = D958027019E6EEEB003C8218 /* Build configuration list for PBXNativeTarget "SwiftHTTPOSX" */; - buildPhases = ( - D958025419E6EEEB003C8218 /* Sources */, - D958025519E6EEEB003C8218 /* Frameworks */, - D958025619E6EEEB003C8218 /* Headers */, - D958025719E6EEEB003C8218 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SwiftHTTPOSX; - productName = SwiftHTTPOSX; - productReference = D958025919E6EEEB003C8218 /* SwiftHTTP.framework */; - productType = "com.apple.product-type.framework"; - }; - D958026219E6EEEB003C8218 /* SwiftHTTPOSXTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = D958027119E6EEEB003C8218 /* Build configuration list for PBXNativeTarget "SwiftHTTPOSXTests" */; - buildPhases = ( - D958025F19E6EEEB003C8218 /* Sources */, - D958026019E6EEEB003C8218 /* Frameworks */, - D958026119E6EEEB003C8218 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - D958026619E6EEEB003C8218 /* PBXTargetDependency */, - ); - name = SwiftHTTPOSXTests; - productName = SwiftHTTPOSXTests; - productReference = D958026319E6EEEB003C8218 /* SwiftHTTPOSXTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 6BFD900D19C8D8B500DD99B6 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0600; + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 0900; ORGANIZATIONNAME = Vluxe; TargetAttributes = { 6BFD901519C8D8B500DD99B6 = { CreatedOnToolsVersion = 6.0; + LastSwiftMigration = 0900; }; 6BFD902019C8D8B500DD99B6 = { CreatedOnToolsVersion = 6.0; }; - D958025819E6EEEB003C8218 = { - CreatedOnToolsVersion = 6.1; - }; - D958026219E6EEEB003C8218 = { - CreatedOnToolsVersion = 6.1; - }; }; }; buildConfigurationList = 6BFD901019C8D8B500DD99B6 /* Build configuration list for PBXProject "SwiftHTTP" */; @@ -272,8 +191,6 @@ targets = ( 6BFD901519C8D8B500DD99B6 /* SwiftHTTP */, 6BFD902019C8D8B500DD99B6 /* SwiftHTTPTests */, - D958025819E6EEEB003C8218 /* SwiftHTTPOSX */, - D958026219E6EEEB003C8218 /* SwiftHTTPOSXTests */, ); }; /* End PBXProject section */ @@ -293,20 +210,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D958025719E6EEEB003C8218 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D958026119E6EEEB003C8218 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -314,11 +217,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6BFD904319C928D900DD99B6 /* HTTPRequestSerializer.swift in Sources */, - 6BFD904419C928D900DD99B6 /* HTTPResponseSerializer.swift in Sources */, - 5CB3204E1B03F5C50050968C /* HTTPSecurity.swift in Sources */, - 6BFD904519C928D900DD99B6 /* HTTPTask.swift in Sources */, - 6BFD903719C8D95C00DD99B6 /* HTTPUpload.swift in Sources */, + 5C135F1C1C47366700AA3A01 /* Request.swift in Sources */, + 5C135F1F1C47366700AA3A01 /* StatusCode.swift in Sources */, + 5C135F191C47366700AA3A01 /* Operation.swift in Sources */, + 5C135F161C47366700AA3A01 /* HTTPSecurity.swift in Sources */, + 5C135F251C47366700AA3A01 /* Upload.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -330,26 +233,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D958025419E6EEEB003C8218 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D958027319E6EF4B003C8218 /* HTTPRequestSerializer.swift in Sources */, - D958027419E6EF4B003C8218 /* HTTPResponseSerializer.swift in Sources */, - 5CB3204F1B03F5C50050968C /* HTTPSecurity.swift in Sources */, - D958027519E6EF4B003C8218 /* HTTPTask.swift in Sources */, - D958027619E6EF4B003C8218 /* HTTPUpload.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D958025F19E6EEEB003C8218 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D958027219E6EF2B003C8218 /* SwiftHTTPTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -358,11 +241,6 @@ target = 6BFD901519C8D8B500DD99B6 /* SwiftHTTP */; targetProxy = 832C8E641B01064F0052A5D7 /* PBXContainerItemProxy */; }; - D958026619E6EEEB003C8218 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D958025819E6EEEB003C8218 /* SwiftHTTPOSX */; - targetProxy = D958026519E6EEEB003C8218 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -374,13 +252,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -388,8 +274,10 @@ CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -408,6 +296,7 @@ PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -422,13 +311,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -438,6 +335,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -448,6 +346,8 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -460,17 +360,28 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = "com.vluxe.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx watchos appletvos appletvsimulator iphoneos iphonesimulator watchsimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALID_ARCHS = "i386 x86_64 arm64 armv7s armv7 armv7k"; + WATCHOS_DEPLOYMENT_TARGET = 2.1; }; name = Debug; }; @@ -479,16 +390,27 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = "com.vluxe.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx watchos appletvos appletvsimulator iphoneos iphonesimulator watchsimulator"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALID_ARCHS = "i386 x86_64 arm64 armv7s armv7 armv7k"; + WATCHOS_DEPLOYMENT_TARGET = 2.1; }; name = Release; }; @@ -505,7 +427,13 @@ ); INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.vluxe.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator macosx"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + VALID_ARCHS = "arm64 armv7 armv7s i386 x86_64"; }; name = Debug; }; @@ -518,89 +446,13 @@ ); INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.vluxe.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; - D958026C19E6EEEB003C8218 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; - PRODUCT_NAME = SwiftHTTP; - SDKROOT = macosx; - SKIP_INSTALL = YES; - }; - name = Debug; - }; - D958026D19E6EEEB003C8218 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - COMBINE_HIDPI_IMAGES = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; - PRODUCT_NAME = SwiftHTTP; - SDKROOT = macosx; - SKIP_INSTALL = YES; - }; - name = Release; - }; - D958026E19E6EEEB003C8218 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - }; - name = Debug; - }; - D958026F19E6EEEB003C8218 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - COMBINE_HIDPI_IMAGES = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; + SDKROOT = ""; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator macosx"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + VALID_ARCHS = "arm64 armv7 armv7s i386 x86_64"; }; name = Release; }; @@ -634,24 +486,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D958027019E6EEEB003C8218 /* Build configuration list for PBXNativeTarget "SwiftHTTPOSX" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D958026C19E6EEEB003C8218 /* Debug */, - D958026D19E6EEEB003C8218 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D958027119E6EEEB003C8218 /* Build configuration list for PBXNativeTarget "SwiftHTTPOSXTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D958026E19E6EEEB003C8218 /* Debug */, - D958026F19E6EEEB003C8218 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 6BFD900D19C8D8B500DD99B6 /* Project object */; diff --git a/SwiftHTTP.xcodeproj/xcshareddata/xcschemes/SwiftHTTP.xcscheme b/SwiftHTTP.xcodeproj/xcshareddata/xcschemes/SwiftHTTP.xcscheme index d5b70eb..9f466aa 100644 --- a/SwiftHTTP.xcodeproj/xcshareddata/xcschemes/SwiftHTTP.xcscheme +++ b/SwiftHTTP.xcodeproj/xcshareddata/xcschemes/SwiftHTTP.xcscheme @@ -1,6 +1,6 @@ + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + + + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -62,15 +63,19 @@ ReferencedContainer = "container:SwiftHTTP.xcodeproj"> + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.vluxe.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Tests/SwiftHTTPTests.swift b/Tests/SwiftHTTPTests.swift index 6acd299..8a1dcb4 100644 --- a/Tests/SwiftHTTPTests.swift +++ b/Tests/SwiftHTTPTests.swift @@ -22,77 +22,90 @@ class SwiftHTTPTests: XCTestCase { } func testGetRequest() { - let expectation = expectationWithDescription("testGetRequest") + let expectation = self.expectation(description: "testGetRequest") - let request = HTTPTask() - request.GET("http://vluxe.io", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - XCTAssert(false, "Failure") + do { + let opt = try HTTP.GET("https://google.com", parameters: nil) + opt.start { response in + if response.error != nil { + XCTAssert(false, "Failure") + } + XCTAssert(true, "Pass") + expectation.fulfill() } - XCTAssert(true, "Pass") - expectation.fulfill() - }) - - waitForExpectationsWithTimeout(30, handler: nil) - } - - func testAuthRequest() { - let expectation = expectationWithDescription("testAuthRequest") - - let request = HTTPTask() - var attempted = false - request.auth = {(challenge: NSURLAuthenticationChallenge) in - if !attempted { - attempted = true - return NSURLCredential(user: "user", password: "passwd", persistence: .ForSession) - } - return nil + } catch { + XCTAssert(false, "Failure") } - request.GET("http://httpbin.org/basic-auth/user/passwd", parameters: nil, completionHandler: {(response: HTTPResponse) in - if let err = response.error { - XCTAssert(false, "Failure") - } - XCTAssert(true, "Pass") - expectation.fulfill() - }) - - waitForExpectationsWithTimeout(30, handler: nil) + waitForExpectations(timeout: 30, handler: nil) } - + + func testGetProgress() { + let expectation1 = expectation(description: "testGetProgressFinished") + let expectation2 = expectation(description: "testGetProgressIncremented") + + do { + let opt = try HTTP.GET("http://photojournal.jpl.nasa.gov/tiff/PIA19330.tif", parameters: nil) + var alreadyCheckedProgressIncremented: Bool = false + opt.progress = { progress in + if progress > 0 && !alreadyCheckedProgressIncremented { + alreadyCheckedProgressIncremented = true + XCTAssert(true, "Pass") + expectation2.fulfill() + } + } + opt.start { response in + if response.error != nil { + XCTAssert(false, "Failure") + } + XCTAssert(true, "Pass") + expectation1.fulfill() + } + } catch { + XCTAssert(false, "Failure") + } + + waitForExpectations(timeout: 30, handler: nil) + } + func testOperationDependencies() { - let expectation1 = expectationWithDescription("testOperationDependencies1") - let expectation2 = expectationWithDescription("testOperationDependencies2") - - let operationQueue = NSOperationQueue() - operationQueue.maxConcurrentOperationCount = 2 + let expectation1 = expectation(description: "testOperationDependencies1") + let expectation2 = expectation(description: "testOperationDependencies2") var operation1Finished = false let urlString1 = "http://photojournal.jpl.nasa.gov/tiff/PIA19330.tif" // (4.32 MB) let urlString2 = "http://photojournal.jpl.nasa.gov/jpeg/PIA19330.jpg" // (0.14 MB) - let request1 = HTTPTask() - let op1 = request1.create(urlString1, method: .GET, parameters: nil, completionHandler: { (response) -> Void in - if let err = response.error { - XCTFail("request1 failed: \(err.localizedDescription)") - } - operation1Finished = true - expectation1.fulfill() - }) + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 2 - let request2 = HTTPTask() - let op2 = request2.create(urlString2, method: .GET, parameters: nil, completionHandler: { (response) -> Void in - if let err = response.error { - XCTFail("request2 failed: \(err.localizedDescription)") + do { + let opt1 = try HTTP.GET(urlString1, parameters: nil) + opt1.onFinish = { response in + if let err = response.error { + XCTFail("request1 failed: \(err.localizedDescription)") + } + operation1Finished = true + expectation1.fulfill() } - XCTAssert(operation1Finished, "Operation 1 did not finish first") - expectation2.fulfill() - }) - - op2?.addDependency(op1!) - operationQueue.addOperation(op1!) - operationQueue.addOperation(op2!) + + let opt2 = try HTTP.GET(urlString2, parameters: nil) + opt2.onFinish = { response in + if let err = response.error { + XCTFail("request2 failed: \(err.localizedDescription)") + } + XCTAssert(operation1Finished, "Operation 1 did not finish first") + expectation2.fulfill() + } + + opt2.addDependency(opt1) + operationQueue.addOperation(opt1) + operationQueue.addOperation(opt2) + + } catch { + XCTAssert(false, "Failure") + } - waitForExpectationsWithTimeout(30, handler: nil) + waitForExpectations(timeout: 30, handler: nil) } }