2 // OAuthSwiftHTTPRequest.swift
5 // Created by Dongri Jin on 6/21/14.
6 // Copyright (c) 2014 Dongri Jin. All rights reserved.
11 let kHTTPHeaderContentType = "Content-Type"
13 open class OAuthSwiftHTTPRequest: NSObject, OAuthSwiftRequestHandle {
15 public typealias SuccessHandler = (_ response: OAuthSwiftResponse) -> Void
16 public typealias FailureHandler = (_ error: OAuthSwiftError) -> Void
18 /// HTTP request method
19 /// https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
20 public enum Method: String {
21 case GET, POST, PUT, DELETE, PATCH, HEAD //, OPTIONS, TRACE, CONNECT
24 return self == .POST || self == .PUT || self == .PATCH
28 /// Where the additional parameters will be injected
29 @objc public enum ParamsLocation: Int {
30 case authorizationHeader, /*FormEncodedBody,*/ requestURIQuery
33 public var config: Config
35 private var request: URLRequest?
36 private var task: URLSessionTask?
37 private var session: URLSession!
39 fileprivate var cancelRequested = false
41 open static var executionContext: (@escaping () -> Void) -> Void = { block in
42 return DispatchQueue.main.async(execute: block)
47 convenience init(url: URL, method: Method = .GET, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, httpBody: Data? = nil, headers: OAuthSwift.Headers = [:], sessionFactory: URLSessionFactory = .default) {
48 self.init(config: Config(url: url, httpMethod: method, httpBody: httpBody, headers: headers, parameters: parameters, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
51 convenience init(request: URLRequest, paramsLocation: ParamsLocation = .authorizationHeader, sessionFactory: URLSessionFactory = .default) {
52 self.init(config: Config(urlRequest: request, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
55 init(config: Config) {
60 func start(success: SuccessHandler?, failure: FailureHandler?) {
61 guard request == nil else { return } // Don't start the same request twice!
63 let successHandler = success
64 let failureHandler = failure
67 self.request = try self.makeRequest()
68 } catch let error as NSError {
69 failureHandler?(OAuthSwiftError.requestCreation(message: error.localizedDescription))
74 OAuthSwiftHTTPRequest.executionContext {
75 // perform lock here to prevent cancel calls on another thread while creating the request
77 defer { objc_sync_exit(self) }
78 if self.cancelRequested {
82 self.session = self.config.sessionFactory.build()
83 let usedRequest = self.request!
85 if self.config.sessionFactory.useDataTaskClosure {
86 let completionHandler: (Data?, URLResponse?, Error?) -> Void = { data, resp, error in
87 OAuthSwiftHTTPRequest.completionHandler(successHandler: success,
88 failureHandler: failure,
94 self.task = self.session.dataTask(with: usedRequest, completionHandler: completionHandler)
96 self.task = self.session.dataTask(with: usedRequest)
100 self.session.finishTasksAndInvalidate()
103 #if !OAUTH_APP_EXTENSIONS
104 UIApplication.shared.isNetworkActivityIndicatorVisible = self.config.sessionFactory.isNetworkActivityIndicatorVisible
110 /// Function called when receiving data from server.
111 public static func completionHandler(successHandler: SuccessHandler?, failureHandler: FailureHandler?, request: URLRequest, data: Data?, resp: URLResponse?, error: Error?) {
113 #if !OAUTH_APP_EXTENSIONS
114 UIApplication.shared.isNetworkActivityIndicatorVisible = false
118 // MARK: failure error returned by server
119 if let error = error {
120 var oauthError: OAuthSwiftError = .requestError(error: error, request: request)
121 let nsError = error as NSError
122 if nsError.code == NSURLErrorCancelled {
123 oauthError = .cancelled
124 } else if nsError.isExpiredToken {
125 oauthError = .tokenExpired(error: error)
128 failureHandler?(oauthError)
132 // MARK: failure no response or data returned by server
133 guard let response = resp as? HTTPURLResponse, let responseData = data else {
134 let badRequestCode = 400
135 let localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(badRequestCode, responseString: "")
136 var userInfo: [String: Any] = [
137 NSLocalizedDescriptionKey: localizedDescription
139 if let response = resp { // there is only no data
140 userInfo[OAuthSwiftError.ResponseKey] = response
142 if let response = resp as? HTTPURLResponse {
143 userInfo["Response-Headers"] = response.allHeaderFields
145 let error = NSError(domain: OAuthSwiftError.Domain, code: badRequestCode, userInfo: userInfo)
146 failureHandler?(.requestError(error:error, request: request))
150 // MARK: failure code > 400
151 guard response.statusCode < 400 else {
152 var localizedDescription = String()
153 let responseString = String(data: responseData, encoding: OAuthSwiftDataEncoding)
155 // Try to get error information from data as json
156 let responseJSON = try? JSONSerialization.jsonObject(with: responseData, options: .mutableContainers)
157 if let responseJSON = responseJSON as? OAuthSwift.Parameters {
158 if let code = responseJSON["error"] as? String, let description = responseJSON["error_description"] as? String {
160 localizedDescription = NSLocalizedString("\(code) \(description)", comment: "")
161 if code == "authorization_pending" {
162 failureHandler?(.authorizationPending)
167 localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(response.statusCode, responseString: String(data: responseData, encoding: OAuthSwiftDataEncoding)!)
170 var userInfo: [String: Any] = [
171 NSLocalizedDescriptionKey: localizedDescription,
172 "Response-Headers": response.allHeaderFields,
173 OAuthSwiftError.ResponseKey: response,
174 OAuthSwiftError.ResponseDataKey: responseData
176 if let string = responseString {
177 userInfo["Response-Body"] = string
179 if let urlString = response.url?.absoluteString {
180 userInfo[NSURLErrorFailingURLErrorKey] = urlString
183 let error = NSError(domain: NSURLErrorDomain, code: response.statusCode, userInfo: userInfo)
184 if error.isExpiredToken {
185 failureHandler?(.tokenExpired(error: error))
187 failureHandler?(.requestError(error: error, request: request))
193 successHandler?(OAuthSwiftResponse(data: responseData, response: response, request: request))
197 // perform lock here to prevent cancel calls on another thread while creating the request
198 objc_sync_enter(self)
199 defer { objc_sync_exit(self) }
200 // either cancel the request if it's already running or set the flag to prohibit creation of the request
204 cancelRequested = true
208 open func makeRequest() throws -> URLRequest {
209 return try OAuthSwiftHTTPRequest.makeRequest(config: self.config)
212 open class func makeRequest(config: Config) throws -> URLRequest {
213 var request = config.urlRequest
214 return try setupRequestForOAuth(request: &request,
215 parameters: config.parameters,
216 dataEncoding: config.dataEncoding,
217 paramsLocation: config.paramsLocation
221 open class func makeRequest(
224 headers: OAuthSwift.Headers,
225 parameters: OAuthSwift.Parameters,
226 dataEncoding: String.Encoding,
228 paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
230 var request = URLRequest(url: url)
231 request.httpMethod = method.rawValue
232 for (key, value) in headers {
233 request.setValue(value, forHTTPHeaderField: key)
236 return try setupRequestForOAuth(
238 parameters: parameters,
239 dataEncoding: dataEncoding,
241 paramsLocation: paramsLocation
245 open class func setupRequestForOAuth(
246 request: inout URLRequest,
247 parameters: OAuthSwift.Parameters,
248 dataEncoding: String.Encoding = OAuthSwiftDataEncoding,
250 paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
252 let finalParameters: OAuthSwift.Parameters
253 switch paramsLocation {
254 case .authorizationHeader:
255 finalParameters = parameters.filter { key, _ in !key.hasPrefix("oauth_") }
256 case .requestURIQuery:
257 finalParameters = parameters
263 if !finalParameters.isEmpty {
264 let charset = dataEncoding.charset
265 let headers = request.allHTTPHeaderFields ?? [:]
266 if request.httpMethod == "GET" || request.httpMethod == "HEAD" || request.httpMethod == "DELETE" {
267 let queryString = finalParameters.urlEncodedQuery
268 let url = request.url!
269 request.url = url.urlByAppending(queryString: queryString)
270 if headers[kHTTPHeaderContentType] == nil {
271 request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
274 if let contentType = headers[kHTTPHeaderContentType], contentType.contains("application/json") {
275 let jsonData = try JSONSerialization.data(withJSONObject: finalParameters, options: [])
276 request.setValue("application/json; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
277 request.httpBody = jsonData
278 } else if let contentType = headers[kHTTPHeaderContentType], contentType.contains("multipart/form-data") {
281 request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
282 let queryString = finalParameters.urlEncodedQuery
283 request.httpBody = queryString.data(using: dataEncoding, allowLossyConversion: true)
293 // MARK: - Request configuraiton
294 extension OAuthSwiftHTTPRequest {
296 /// Configuration for request
297 public struct Config {
299 /// URLRequest (url, method, ...)
300 public var urlRequest: URLRequest
301 /// These parameters are either added to the query string for GET, HEAD and DELETE requests or
302 /// used as the http body in case of POST, PUT or PATCH requests.
304 /// If used in the body they are either encoded as JSON or as encoded plaintext based on the Content-Type header field.
305 public var parameters: OAuthSwift.Parameters
306 public let paramsLocation: ParamsLocation
307 public let dataEncoding: String.Encoding
308 public let sessionFactory: URLSessionFactory
311 public var httpMethod: Method {
312 if let requestMethod = urlRequest.httpMethod {
313 return Method(rawValue: requestMethod) ?? .GET
318 public var url: Foundation.URL? {
319 return urlRequest.url
323 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) {
324 var urlRequest = URLRequest(url: url)
325 urlRequest.httpMethod = httpMethod.rawValue
326 urlRequest.httpBody = httpBody
327 urlRequest.allHTTPHeaderFields = headers
328 urlRequest.timeoutInterval = timeoutInterval
329 urlRequest.httpShouldHandleCookies = httpShouldHandleCookies
330 self.init(urlRequest: urlRequest, parameters: parameters, paramsLocation: paramsLocation, dataEncoding: dataEncoding, sessionFactory: sessionFactory)
333 public init(urlRequest: URLRequest, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, dataEncoding: String.Encoding = OAuthSwiftDataEncoding, sessionFactory: URLSessionFactory = .default) {
334 self.urlRequest = urlRequest
335 self.parameters = parameters
336 self.paramsLocation = paramsLocation
337 self.dataEncoding = dataEncoding
338 self.sessionFactory = sessionFactory
341 /// Modify request with authentification
342 public mutating func updateRequest(credential: OAuthSwiftCredential) {
343 let method = self.httpMethod
344 let url = self.urlRequest.url!
345 let headers: OAuthSwift.Headers = self.urlRequest.allHTTPHeaderFields ?? [:]
346 let paramsLocation = self.paramsLocation
347 let parameters = self.parameters
349 var signatureUrl = url
350 var signatureParameters = parameters
352 // Check if body must be hashed (oauth1)
353 let body: Data? = nil
355 if let contentType = headers[kHTTPHeaderContentType]?.lowercased() {
357 if contentType.contains("application/json") {
358 // TODO: oauth_body_hash create body before signing if implementing body hashing
360 let jsonData: Data = try JSONSerialization.jsonObject(parameters, options: [])
361 request.HTTPBody = jsonData
362 requestHeaders["Content-Length"] = "\(jsonData.length)"
368 signatureParameters = [:] // parameters are not used for general signature (could only be used for body hashing
370 // else other type are not supported, see setupRequestForOAuth()
374 // Need to account for the fact that some consumers will have additional parameters on the
375 // querystring, including in the case of fetching a request token. Especially in the case of
376 // additional parameters on the request, authorize, or access token exchanges, we need to
377 // normalize the URL and add to the parametes collection.
379 var queryStringParameters = OAuthSwift.Parameters()
380 var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false )
381 if let queryItems = urlComponents?.queryItems {
382 for queryItem in queryItems {
383 let value = queryItem.value?.safeStringByRemovingPercentEncoding ?? ""
384 queryStringParameters.updateValue(value, forKey: queryItem.name)
388 // According to the OAuth1.0a spec, the url used for signing is ONLY scheme, path, and query
389 if !queryStringParameters.isEmpty {
390 urlComponents?.query = nil
391 // This is safe to unwrap because these just came from an NSURL
392 signatureUrl = urlComponents?.url ?? url
394 signatureParameters = signatureParameters.join(queryStringParameters)
396 var requestHeaders = OAuthSwift.Headers()
397 switch paramsLocation {
398 case .authorizationHeader:
399 //Add oauth parameters in the Authorization header
400 requestHeaders += credential.makeHeaders(signatureUrl, method: method, parameters: signatureParameters, body: body)
401 case .requestURIQuery:
402 //Add oauth parameters as request parameters
403 self.parameters += credential.authorizationParametersWithSignature(method: method, url: signatureUrl, parameters: signatureParameters, body: body)
406 self.urlRequest.allHTTPHeaderFields = requestHeaders + headers
412 // MARK: - session configuration
414 /// configure how URLSession is initialized
415 public struct URLSessionFactory {
417 public static let `default` = URLSessionFactory()
419 public var configuration = URLSessionConfiguration.default
420 public var queue = OperationQueue.main
421 /// An optional delegate for the URLSession
422 public weak var delegate: URLSessionDelegate?
424 /// Monitor session: see UIApplication.shared.isNetworkActivityIndicatorVisible
425 public var isNetworkActivityIndicatorVisible = true
427 /// By default use a closure to receive data from server.
428 /// If you set to false, you must in `delegate` take care of server response.
429 /// and maybe call in delegate `OAuthSwiftHTTPRequest.completionHandler`
430 public var useDataTaskClosure = true
432 /// Create a new URLSession
433 func build() -> URLSession {
434 return URLSession(configuration: self.configuration, delegate: self.delegate, delegateQueue: self.queue)
438 // MARK: - status code mapping
440 extension OAuthSwiftHTTPRequest {
442 class func descriptionForHTTPStatus(_ status: Int, responseString: String) -> String {
444 var s = "HTTP Status \(status)"
446 var description: String?
447 // http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
448 if status == 400 { description = "Bad Request" }
449 if status == 401 { description = "Unauthorized" }
450 if status == 402 { description = "Payment Required" }
451 if status == 403 { description = "Forbidden" }
452 if status == 404 { description = "Not Found" }
453 if status == 405 { description = "Method Not Allowed" }
454 if status == 406 { description = "Not Acceptable" }
455 if status == 407 { description = "Proxy Authentication Required" }
456 if status == 408 { description = "Request Timeout" }
457 if status == 409 { description = "Conflict" }
458 if status == 410 { description = "Gone" }
459 if status == 411 { description = "Length Required" }
460 if status == 412 { description = "Precondition Failed" }
461 if status == 413 { description = "Payload Too Large" }
462 if status == 414 { description = "URI Too Long" }
463 if status == 415 { description = "Unsupported Media Type" }
464 if status == 416 { description = "Requested Range Not Satisfiable" }
465 if status == 417 { description = "Expectation Failed" }
466 if status == 422 { description = "Unprocessable Entity" }
467 if status == 423 { description = "Locked" }
468 if status == 424 { description = "Failed Dependency" }
469 if status == 425 { description = "Unassigned" }
470 if status == 426 { description = "Upgrade Required" }
471 if status == 427 { description = "Unassigned" }
472 if status == 428 { description = "Precondition Required" }
473 if status == 429 { description = "Too Many Requests" }
474 if status == 430 { description = "Unassigned" }
475 if status == 431 { description = "Request Header Fields Too Large" }
476 if status == 432 { description = "Unassigned" }
477 if status == 500 { description = "Internal Server Error" }
478 if status == 501 { description = "Not Implemented" }
479 if status == 502 { description = "Bad Gateway" }
480 if status == 503 { description = "Service Unavailable" }
481 if status == 504 { description = "Gateway Timeout" }
482 if status == 505 { description = "HTTP Version Not Supported" }
483 if status == 506 { description = "Variant Also Negotiates" }
484 if status == 507 { description = "Insufficient Storage" }
485 if status == 508 { description = "Loop Detected" }
486 if status == 509 { description = "Unassigned" }
487 if status == 510 { description = "Not Extended" }
488 if status == 511 { description = "Network Authentication Required" }
490 if description != nil {
491 s += ": " + description! + ", Response: " + responseString