--- /dev/null
+//
+// OAuthSwiftHTTPRequest.swift
+// OAuthSwift
+//
+// Created by Dongri Jin on 6/21/14.
+// Copyright (c) 2014 Dongri Jin. All rights reserved.
+//
+
+import Foundation
+
+let kHTTPHeaderContentType = "Content-Type"
+
+open class OAuthSwiftHTTPRequest: NSObject, OAuthSwiftRequestHandle {
+
+ public typealias SuccessHandler = (_ response: OAuthSwiftResponse) -> Void
+ public typealias FailureHandler = (_ error: OAuthSwiftError) -> Void
+
+ /// HTTP request method
+ /// https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
+ public enum Method: String {
+ case GET, POST, PUT, DELETE, PATCH, HEAD //, OPTIONS, TRACE, CONNECT
+
+ var isBody: Bool {
+ return self == .POST || self == .PUT || self == .PATCH
+ }
+ }
+
+ /// Where the additional parameters will be injected
+ @objc public enum ParamsLocation: Int {
+ case authorizationHeader, /*FormEncodedBody,*/ requestURIQuery
+ }
+
+ public var config: Config
+
+ private var request: URLRequest?
+ private var task: URLSessionTask?
+ private var session: URLSession!
+
+ fileprivate var cancelRequested = false
+
+ open static var executionContext: (@escaping () -> Void) -> Void = { block in
+ return DispatchQueue.main.async(execute: block)
+ }
+
+ // MARK: INIT
+
+ convenience init(url: URL, method: Method = .GET, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, httpBody: Data? = nil, headers: OAuthSwift.Headers = [:], sessionFactory: URLSessionFactory = .default) {
+ self.init(config: Config(url: url, httpMethod: method, httpBody: httpBody, headers: headers, parameters: parameters, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
+ }
+
+ convenience init(request: URLRequest, paramsLocation: ParamsLocation = .authorizationHeader, sessionFactory: URLSessionFactory = .default) {
+ self.init(config: Config(urlRequest: request, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
+ }
+
+ init(config: Config) {
+ self.config = config
+ }
+
+ /// START request
+ func start(success: SuccessHandler?, failure: FailureHandler?) {
+ guard request == nil else { return } // Don't start the same request twice!
+
+ let successHandler = success
+ let failureHandler = failure
+
+ do {
+ self.request = try self.makeRequest()
+ } catch let error as NSError {
+ failureHandler?(OAuthSwiftError.requestCreation(message: error.localizedDescription))
+ self.request = nil
+ return
+ }
+
+ OAuthSwiftHTTPRequest.executionContext {
+ // perform lock here to prevent cancel calls on another thread while creating the request
+ objc_sync_enter(self)
+ defer { objc_sync_exit(self) }
+ if self.cancelRequested {
+ return
+ }
+
+ self.session = self.config.sessionFactory.build()
+ let usedRequest = self.request!
+
+ if self.config.sessionFactory.useDataTaskClosure {
+ let completionHandler: (Data?, URLResponse?, Error?) -> Void = { data, resp, error in
+ OAuthSwiftHTTPRequest.completionHandler(successHandler: success,
+ failureHandler: failure,
+ request: usedRequest,
+ data: data,
+ resp: resp,
+ error: error)
+ }
+ self.task = self.session.dataTask(with: usedRequest, completionHandler: completionHandler)
+ } else {
+ self.task = self.session.dataTask(with: usedRequest)
+ }
+
+ self.task?.resume()
+ self.session.finishTasksAndInvalidate()
+
+ #if os(iOS)
+ #if !OAUTH_APP_EXTENSIONS
+ UIApplication.shared.isNetworkActivityIndicatorVisible = self.config.sessionFactory.isNetworkActivityIndicatorVisible
+ #endif
+ #endif
+ }
+ }
+
+ /// Function called when receiving data from server.
+ public static func completionHandler(successHandler: SuccessHandler?, failureHandler: FailureHandler?, request: URLRequest, data: Data?, resp: URLResponse?, error: Error?) {
+ #if os(iOS)
+ #if !OAUTH_APP_EXTENSIONS
+ UIApplication.shared.isNetworkActivityIndicatorVisible = false
+ #endif
+ #endif
+
+ // MARK: failure error returned by server
+ if let error = error {
+ var oauthError: OAuthSwiftError = .requestError(error: error, request: request)
+ let nsError = error as NSError
+ if nsError.code == NSURLErrorCancelled {
+ oauthError = .cancelled
+ } else if nsError.isExpiredToken {
+ oauthError = .tokenExpired(error: error)
+ }
+
+ failureHandler?(oauthError)
+ return
+ }
+
+ // MARK: failure no response or data returned by server
+ guard let response = resp as? HTTPURLResponse, let responseData = data else {
+ let badRequestCode = 400
+ let localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(badRequestCode, responseString: "")
+ var userInfo: [String: Any] = [
+ NSLocalizedDescriptionKey: localizedDescription
+ ]
+ if let response = resp { // there is only no data
+ userInfo[OAuthSwiftError.ResponseKey] = response
+ }
+ if let response = resp as? HTTPURLResponse {
+ userInfo["Response-Headers"] = response.allHeaderFields
+ }
+ let error = NSError(domain: OAuthSwiftError.Domain, code: badRequestCode, userInfo: userInfo)
+ failureHandler?(.requestError(error:error, request: request))
+ return
+ }
+
+ // MARK: failure code > 400
+ guard response.statusCode < 400 else {
+ var localizedDescription = String()
+ let responseString = String(data: responseData, encoding: OAuthSwiftDataEncoding)
+
+ // Try to get error information from data as json
+ let responseJSON = try? JSONSerialization.jsonObject(with: responseData, options: .mutableContainers)
+ if let responseJSON = responseJSON as? OAuthSwift.Parameters {
+ if let code = responseJSON["error"] as? String, let description = responseJSON["error_description"] as? String {
+
+ localizedDescription = NSLocalizedString("\(code) \(description)", comment: "")
+ if code == "authorization_pending" {
+ failureHandler?(.authorizationPending)
+ return
+ }
+ }
+ } else {
+ localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(response.statusCode, responseString: String(data: responseData, encoding: OAuthSwiftDataEncoding)!)
+ }
+
+ var userInfo: [String: Any] = [
+ NSLocalizedDescriptionKey: localizedDescription,
+ "Response-Headers": response.allHeaderFields,
+ OAuthSwiftError.ResponseKey: response,
+ OAuthSwiftError.ResponseDataKey: responseData
+ ]
+ if let string = responseString {
+ userInfo["Response-Body"] = string
+ }
+ if let urlString = response.url?.absoluteString {
+ userInfo[NSURLErrorFailingURLErrorKey] = urlString
+ }
+
+ let error = NSError(domain: NSURLErrorDomain, code: response.statusCode, userInfo: userInfo)
+ if error.isExpiredToken {
+ failureHandler?(.tokenExpired(error: error))
+ } else {
+ failureHandler?(.requestError(error: error, request: request))
+ }
+ return
+ }
+
+ // MARK: success
+ successHandler?(OAuthSwiftResponse(data: responseData, response: response, request: request))
+ }
+
+ open func cancel() {
+ // perform lock here to prevent cancel calls on another thread while creating the request
+ objc_sync_enter(self)
+ defer { objc_sync_exit(self) }
+ // either cancel the request if it's already running or set the flag to prohibit creation of the request
+ if let task = task {
+ task.cancel()
+ } else {
+ cancelRequested = true
+ }
+ }
+
+ open func makeRequest() throws -> URLRequest {
+ return try OAuthSwiftHTTPRequest.makeRequest(config: self.config)
+ }
+
+ open class func makeRequest(config: Config) throws -> URLRequest {
+ var request = config.urlRequest
+ return try setupRequestForOAuth(request: &request,
+ parameters: config.parameters,
+ dataEncoding: config.dataEncoding,
+ paramsLocation: config.paramsLocation
+ )
+ }
+
+ open class func makeRequest(
+ url: Foundation.URL,
+ method: Method,
+ headers: OAuthSwift.Headers,
+ parameters: OAuthSwift.Parameters,
+ dataEncoding: String.Encoding,
+ body: Data? = nil,
+ paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
+
+ var request = URLRequest(url: url)
+ request.httpMethod = method.rawValue
+ for (key, value) in headers {
+ request.setValue(value, forHTTPHeaderField: key)
+ }
+
+ return try setupRequestForOAuth(
+ request: &request,
+ parameters: parameters,
+ dataEncoding: dataEncoding,
+ body: body,
+ paramsLocation: paramsLocation
+ )
+ }
+
+ open class func setupRequestForOAuth(
+ request: inout URLRequest,
+ parameters: OAuthSwift.Parameters,
+ dataEncoding: String.Encoding = OAuthSwiftDataEncoding,
+ body: Data? = nil,
+ paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
+
+ let finalParameters: OAuthSwift.Parameters
+ switch paramsLocation {
+ case .authorizationHeader:
+ finalParameters = parameters.filter { key, _ in !key.hasPrefix("oauth_") }
+ case .requestURIQuery:
+ finalParameters = parameters
+ }
+
+ if let b = body {
+ request.httpBody = b
+ } else {
+ if !finalParameters.isEmpty {
+ let charset = dataEncoding.charset
+ let headers = request.allHTTPHeaderFields ?? [:]
+ if request.httpMethod == "GET" || request.httpMethod == "HEAD" || request.httpMethod == "DELETE" {
+ let queryString = finalParameters.urlEncodedQuery
+ let url = request.url!
+ request.url = url.urlByAppending(queryString: queryString)
+ if headers[kHTTPHeaderContentType] == nil {
+ request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
+ }
+ } else {
+ if let contentType = headers[kHTTPHeaderContentType], contentType.contains("application/json") {
+ let jsonData = try JSONSerialization.data(withJSONObject: finalParameters, options: [])
+ request.setValue("application/json; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
+ request.httpBody = jsonData
+ } else if let contentType = headers[kHTTPHeaderContentType], contentType.contains("multipart/form-data") {
+ // snip
+ } else {
+ request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
+ let queryString = finalParameters.urlEncodedQuery
+ request.httpBody = queryString.data(using: dataEncoding, allowLossyConversion: true)
+ }
+ }
+ }
+ }
+ return request
+ }
+
+}
+
+// MARK: - Request configuraiton
+extension OAuthSwiftHTTPRequest {
+
+ /// Configuration for request
+ public struct Config {
+
+ /// URLRequest (url, method, ...)
+ public var urlRequest: URLRequest
+ /// These parameters are either added to the query string for GET, HEAD and DELETE requests or
+ /// used as the http body in case of POST, PUT or PATCH requests.
+ ///
+ /// If used in the body they are either encoded as JSON or as encoded plaintext based on the Content-Type header field.
+ public var parameters: OAuthSwift.Parameters
+ public let paramsLocation: ParamsLocation
+ public let dataEncoding: String.Encoding
+ public let sessionFactory: URLSessionFactory
+
+ /// Shortcut
+ public var httpMethod: Method {
+ if let requestMethod = urlRequest.httpMethod {
+ return Method(rawValue: requestMethod) ?? .GET
+ }
+ return .GET
+ }
+
+ public var url: Foundation.URL? {
+ return urlRequest.url
+ }
+
+ // MARK: init
+ public init(url: URL, httpMethod: Method = .GET, httpBody: Data? = nil, headers: OAuthSwift.Headers = [:], timeoutInterval: TimeInterval = 60, httpShouldHandleCookies: Bool = false, parameters: OAuthSwift.Parameters, paramsLocation: ParamsLocation = .authorizationHeader, dataEncoding: String.Encoding = OAuthSwiftDataEncoding, sessionFactory: URLSessionFactory = .default) {
+ var urlRequest = URLRequest(url: url)
+ urlRequest.httpMethod = httpMethod.rawValue
+ urlRequest.httpBody = httpBody
+ urlRequest.allHTTPHeaderFields = headers
+ urlRequest.timeoutInterval = timeoutInterval
+ urlRequest.httpShouldHandleCookies = httpShouldHandleCookies
+ self.init(urlRequest: urlRequest, parameters: parameters, paramsLocation: paramsLocation, dataEncoding: dataEncoding, sessionFactory: sessionFactory)
+ }
+
+ public init(urlRequest: URLRequest, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, dataEncoding: String.Encoding = OAuthSwiftDataEncoding, sessionFactory: URLSessionFactory = .default) {
+ self.urlRequest = urlRequest
+ self.parameters = parameters
+ self.paramsLocation = paramsLocation
+ self.dataEncoding = dataEncoding
+ self.sessionFactory = sessionFactory
+ }
+
+ /// Modify request with authentification
+ public mutating func updateRequest(credential: OAuthSwiftCredential) {
+ let method = self.httpMethod
+ let url = self.urlRequest.url!
+ let headers: OAuthSwift.Headers = self.urlRequest.allHTTPHeaderFields ?? [:]
+ let paramsLocation = self.paramsLocation
+ let parameters = self.parameters
+
+ var signatureUrl = url
+ var signatureParameters = parameters
+
+ // Check if body must be hashed (oauth1)
+ let body: Data? = nil
+ if method.isBody {
+ if let contentType = headers[kHTTPHeaderContentType]?.lowercased() {
+
+ if contentType.contains("application/json") {
+ // TODO: oauth_body_hash create body before signing if implementing body hashing
+ /*do {
+ let jsonData: Data = try JSONSerialization.jsonObject(parameters, options: [])
+ request.HTTPBody = jsonData
+ requestHeaders["Content-Length"] = "\(jsonData.length)"
+ body = jsonData
+ }
+ catch {
+ }*/
+
+ signatureParameters = [:] // parameters are not used for general signature (could only be used for body hashing
+ }
+ // else other type are not supported, see setupRequestForOAuth()
+ }
+ }
+
+ // Need to account for the fact that some consumers will have additional parameters on the
+ // querystring, including in the case of fetching a request token. Especially in the case of
+ // additional parameters on the request, authorize, or access token exchanges, we need to
+ // normalize the URL and add to the parametes collection.
+
+ var queryStringParameters = OAuthSwift.Parameters()
+ var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false )
+ if let queryItems = urlComponents?.queryItems {
+ for queryItem in queryItems {
+ let value = queryItem.value?.safeStringByRemovingPercentEncoding ?? ""
+ queryStringParameters.updateValue(value, forKey: queryItem.name)
+ }
+ }
+
+ // According to the OAuth1.0a spec, the url used for signing is ONLY scheme, path, and query
+ if !queryStringParameters.isEmpty {
+ urlComponents?.query = nil
+ // This is safe to unwrap because these just came from an NSURL
+ signatureUrl = urlComponents?.url ?? url
+ }
+ signatureParameters = signatureParameters.join(queryStringParameters)
+
+ var requestHeaders = OAuthSwift.Headers()
+ switch paramsLocation {
+ case .authorizationHeader:
+ //Add oauth parameters in the Authorization header
+ requestHeaders += credential.makeHeaders(signatureUrl, method: method, parameters: signatureParameters, body: body)
+ case .requestURIQuery:
+ //Add oauth parameters as request parameters
+ self.parameters += credential.authorizationParametersWithSignature(method: method, url: signatureUrl, parameters: signatureParameters, body: body)
+ }
+
+ self.urlRequest.allHTTPHeaderFields = requestHeaders + headers
+ }
+
+ }
+}
+
+// MARK: - session configuration
+
+/// configure how URLSession is initialized
+public struct URLSessionFactory {
+
+ public static let `default` = URLSessionFactory()
+
+ public var configuration = URLSessionConfiguration.default
+ public var queue = OperationQueue.main
+ /// An optional delegate for the URLSession
+ public weak var delegate: URLSessionDelegate?
+
+ /// Monitor session: see UIApplication.shared.isNetworkActivityIndicatorVisible
+ public var isNetworkActivityIndicatorVisible = true
+
+ /// By default use a closure to receive data from server.
+ /// If you set to false, you must in `delegate` take care of server response.
+ /// and maybe call in delegate `OAuthSwiftHTTPRequest.completionHandler`
+ public var useDataTaskClosure = true
+
+ /// Create a new URLSession
+ func build() -> URLSession {
+ return URLSession(configuration: self.configuration, delegate: self.delegate, delegateQueue: self.queue)
+ }
+}
+
+// MARK: - status code mapping
+
+extension OAuthSwiftHTTPRequest {
+
+ class func descriptionForHTTPStatus(_ status: Int, responseString: String) -> String {
+
+ var s = "HTTP Status \(status)"
+
+ var description: String?
+ // http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ if status == 400 { description = "Bad Request" }
+ if status == 401 { description = "Unauthorized" }
+ if status == 402 { description = "Payment Required" }
+ if status == 403 { description = "Forbidden" }
+ if status == 404 { description = "Not Found" }
+ if status == 405 { description = "Method Not Allowed" }
+ if status == 406 { description = "Not Acceptable" }
+ if status == 407 { description = "Proxy Authentication Required" }
+ if status == 408 { description = "Request Timeout" }
+ if status == 409 { description = "Conflict" }
+ if status == 410 { description = "Gone" }
+ if status == 411 { description = "Length Required" }
+ if status == 412 { description = "Precondition Failed" }
+ if status == 413 { description = "Payload Too Large" }
+ if status == 414 { description = "URI Too Long" }
+ if status == 415 { description = "Unsupported Media Type" }
+ if status == 416 { description = "Requested Range Not Satisfiable" }
+ if status == 417 { description = "Expectation Failed" }
+ if status == 422 { description = "Unprocessable Entity" }
+ if status == 423 { description = "Locked" }
+ if status == 424 { description = "Failed Dependency" }
+ if status == 425 { description = "Unassigned" }
+ if status == 426 { description = "Upgrade Required" }
+ if status == 427 { description = "Unassigned" }
+ if status == 428 { description = "Precondition Required" }
+ if status == 429 { description = "Too Many Requests" }
+ if status == 430 { description = "Unassigned" }
+ if status == 431 { description = "Request Header Fields Too Large" }
+ if status == 432 { description = "Unassigned" }
+ if status == 500 { description = "Internal Server Error" }
+ if status == 501 { description = "Not Implemented" }
+ if status == 502 { description = "Bad Gateway" }
+ if status == 503 { description = "Service Unavailable" }
+ if status == 504 { description = "Gateway Timeout" }
+ if status == 505 { description = "HTTP Version Not Supported" }
+ if status == 506 { description = "Variant Also Negotiates" }
+ if status == 507 { description = "Insufficient Storage" }
+ if status == 508 { description = "Loop Detected" }
+ if status == 509 { description = "Unassigned" }
+ if status == 510 { description = "Not Extended" }
+ if status == 511 { description = "Network Authentication Required" }
+
+ if description != nil {
+ s += ": " + description! + ", Response: " + responseString
+ }
+
+ return s
+ }
+
+}