////////////////////////////////////////////////////////////////////////////////////////////////// // // 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 resObj = resSerializer.responseObjectFromResponse(response, 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 !url.hasPrefix("http") && self.baseURL != nil { var split = url.hasPrefix("/") ? "" : "/" urlVal = "\(self.baseURL!)\(split)\(url)" } if let u = NSURL(string: urlVal) { 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) { } }