2015-09-13 23:37:18 +02:00

555 lines
23 KiB
Swift

//////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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<String,String>?
/// 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<String,BackgroundBlocks>()
//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<String,AnyObject>!, 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<String,String>
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<String,AnyObject>?, 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<String,AnyObject>?, 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<String,AnyObject>?, 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<String,AnyObject>?, 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<String,AnyObject>?, 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<String,AnyObject>?, 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<String,AnyObject>?,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<String,AnyObject>?,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<String,AnyObject>!) -> (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<String,String>
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) {
}
}