added iOS source code
[wl-app.git] / iOS / Pods / OAuthSwift / Sources / OAuthSwiftHTTPRequest.swift
1 //
2 //  OAuthSwiftHTTPRequest.swift
3 //  OAuthSwift
4 //
5 //  Created by Dongri Jin on 6/21/14.
6 //  Copyright (c) 2014 Dongri Jin. All rights reserved.
7 //
8
9 import Foundation
10
11 let kHTTPHeaderContentType = "Content-Type"
12
13 open class OAuthSwiftHTTPRequest: NSObject, OAuthSwiftRequestHandle {
14
15     public typealias SuccessHandler = (_ response: OAuthSwiftResponse) -> Void
16     public typealias FailureHandler = (_ error: OAuthSwiftError) -> Void
17
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
22
23         var isBody: Bool {
24             return self == .POST || self == .PUT || self == .PATCH
25         }
26     }
27
28     /// Where the additional parameters will be injected
29     @objc public enum ParamsLocation: Int {
30         case authorizationHeader, /*FormEncodedBody,*/ requestURIQuery
31     }
32
33     public var config: Config
34
35     private var request: URLRequest?
36     private var task: URLSessionTask?
37     private var session: URLSession!
38
39     fileprivate var cancelRequested = false
40
41     open static var executionContext: (@escaping () -> Void) -> Void = { block in
42         return DispatchQueue.main.async(execute: block)
43     }
44
45     // MARK: INIT
46
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))
49     }
50
51     convenience init(request: URLRequest, paramsLocation: ParamsLocation = .authorizationHeader, sessionFactory: URLSessionFactory = .default) {
52         self.init(config: Config(urlRequest: request, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
53     }
54
55     init(config: Config) {
56         self.config = config
57     }
58
59     /// START request
60     func start(success: SuccessHandler?, failure: FailureHandler?) {
61         guard request == nil else { return } // Don't start the same request twice!
62
63         let successHandler = success
64         let failureHandler = failure
65
66         do {
67             self.request = try self.makeRequest()
68         } catch let error as NSError {
69             failureHandler?(OAuthSwiftError.requestCreation(message: error.localizedDescription))
70             self.request = nil
71             return
72         }
73
74         OAuthSwiftHTTPRequest.executionContext {
75             // perform lock here to prevent cancel calls on another thread while creating the request
76             objc_sync_enter(self)
77             defer { objc_sync_exit(self) }
78             if self.cancelRequested {
79                 return
80             }
81
82             self.session = self.config.sessionFactory.build()
83             let usedRequest = self.request!
84
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,
89                                                             request: usedRequest,
90                                                             data: data,
91                                                             resp: resp,
92                                                             error: error)
93                 }
94                 self.task = self.session.dataTask(with: usedRequest, completionHandler: completionHandler)
95             } else {
96                 self.task = self.session.dataTask(with: usedRequest)
97             }
98
99             self.task?.resume()
100             self.session.finishTasksAndInvalidate()
101
102             #if os(iOS)
103                 #if !OAUTH_APP_EXTENSIONS
104                     UIApplication.shared.isNetworkActivityIndicatorVisible = self.config.sessionFactory.isNetworkActivityIndicatorVisible
105                 #endif
106             #endif
107         }
108     }
109
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?) {
112         #if os(iOS)
113         #if !OAUTH_APP_EXTENSIONS
114         UIApplication.shared.isNetworkActivityIndicatorVisible = false
115         #endif
116         #endif
117
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)
126             }
127
128             failureHandler?(oauthError)
129             return
130         }
131
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
138             ]
139             if let response = resp { // there is only no data
140                 userInfo[OAuthSwiftError.ResponseKey] = response
141             }
142             if let response = resp as? HTTPURLResponse {
143                 userInfo["Response-Headers"] = response.allHeaderFields
144             }
145             let error = NSError(domain: OAuthSwiftError.Domain, code: badRequestCode, userInfo: userInfo)
146             failureHandler?(.requestError(error:error, request: request))
147             return
148         }
149
150         // MARK: failure code > 400
151         guard response.statusCode < 400 else {
152             var localizedDescription = String()
153             let responseString = String(data: responseData, encoding: OAuthSwiftDataEncoding)
154
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 {
159
160                     localizedDescription = NSLocalizedString("\(code) \(description)", comment: "")
161                     if code == "authorization_pending" {
162                         failureHandler?(.authorizationPending)
163                         return
164                     }
165                 }
166             } else {
167                 localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(response.statusCode, responseString: String(data: responseData, encoding: OAuthSwiftDataEncoding)!)
168             }
169
170             var userInfo: [String: Any] = [
171                 NSLocalizedDescriptionKey: localizedDescription,
172                 "Response-Headers": response.allHeaderFields,
173                 OAuthSwiftError.ResponseKey: response,
174                 OAuthSwiftError.ResponseDataKey: responseData
175             ]
176             if let string = responseString {
177                 userInfo["Response-Body"] = string
178             }
179             if let urlString = response.url?.absoluteString {
180                 userInfo[NSURLErrorFailingURLErrorKey] = urlString
181             }
182
183             let error = NSError(domain: NSURLErrorDomain, code: response.statusCode, userInfo: userInfo)
184             if error.isExpiredToken {
185                 failureHandler?(.tokenExpired(error: error))
186             } else {
187                 failureHandler?(.requestError(error: error, request: request))
188             }
189             return
190         }
191
192         // MARK: success
193         successHandler?(OAuthSwiftResponse(data: responseData, response: response, request: request))
194     }
195
196     open func cancel() {
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
201         if let task = task {
202             task.cancel()
203         } else {
204             cancelRequested = true
205         }
206     }
207
208     open func makeRequest() throws -> URLRequest {
209         return try OAuthSwiftHTTPRequest.makeRequest(config: self.config)
210     }
211
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
218         )
219     }
220
221     open class func makeRequest(
222         url: Foundation.URL,
223         method: Method,
224         headers: OAuthSwift.Headers,
225         parameters: OAuthSwift.Parameters,
226         dataEncoding: String.Encoding,
227         body: Data? = nil,
228         paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
229
230         var request = URLRequest(url: url)
231         request.httpMethod = method.rawValue
232         for (key, value) in headers {
233             request.setValue(value, forHTTPHeaderField: key)
234         }
235
236         return try setupRequestForOAuth(
237             request: &request,
238             parameters: parameters,
239             dataEncoding: dataEncoding,
240             body: body,
241             paramsLocation: paramsLocation
242         )
243     }
244
245     open class func setupRequestForOAuth(
246         request: inout URLRequest,
247         parameters: OAuthSwift.Parameters,
248         dataEncoding: String.Encoding = OAuthSwiftDataEncoding,
249         body: Data? = nil,
250         paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {
251
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
258         }
259
260         if let b = body {
261             request.httpBody = b
262         } else {
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)
272                     }
273                 } else {
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") {
279                     // snip
280                     } else {
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)
284                     }
285                 }
286             }
287         }
288         return request
289     }
290
291 }
292
293 // MARK: - Request configuraiton
294 extension OAuthSwiftHTTPRequest {
295
296     /// Configuration for request
297     public struct Config {
298
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.
303         ///
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
309
310         /// Shortcut
311         public var httpMethod: Method {
312             if let requestMethod = urlRequest.httpMethod {
313                 return Method(rawValue: requestMethod) ?? .GET
314             }
315             return .GET
316         }
317
318         public var url: Foundation.URL? {
319             return urlRequest.url
320         }
321
322         // MARK: init
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)
331         }
332
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
339         }
340
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
348
349             var signatureUrl = url
350             var signatureParameters = parameters
351
352             // Check if body must be hashed (oauth1)
353             let body: Data? = nil
354             if method.isBody {
355                 if let contentType = headers[kHTTPHeaderContentType]?.lowercased() {
356
357                     if contentType.contains("application/json") {
358                         // TODO: oauth_body_hash create body before signing if implementing body hashing
359                         /*do {
360                          let jsonData: Data = try JSONSerialization.jsonObject(parameters, options: [])
361                          request.HTTPBody = jsonData
362                          requestHeaders["Content-Length"] = "\(jsonData.length)"
363                          body = jsonData
364                          }
365                          catch {
366                          }*/
367
368                         signatureParameters = [:] // parameters are not used for general signature (could only be used for body hashing
369                     }
370                     // else other type are not supported, see setupRequestForOAuth()
371                 }
372             }
373
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.
378
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)
385                 }
386             }
387
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
393             }
394             signatureParameters = signatureParameters.join(queryStringParameters)
395
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)
404             }
405
406             self.urlRequest.allHTTPHeaderFields = requestHeaders + headers
407         }
408
409     }
410 }
411
412 // MARK: - session configuration
413
414 /// configure how URLSession is initialized
415 public struct URLSessionFactory {
416
417     public static let `default` = URLSessionFactory()
418
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?
423
424     /// Monitor session: see UIApplication.shared.isNetworkActivityIndicatorVisible
425     public var isNetworkActivityIndicatorVisible = true
426
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
431
432     /// Create a new URLSession
433     func build() -> URLSession {
434         return URLSession(configuration: self.configuration, delegate: self.delegate, delegateQueue: self.queue)
435     }
436 }
437
438 // MARK: - status code mapping
439
440 extension OAuthSwiftHTTPRequest {
441
442     class func descriptionForHTTPStatus(_ status: Int, responseString: String) -> String {
443
444         var s = "HTTP Status \(status)"
445
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" }
489
490         if description != nil {
491             s += ": " + description! + ", Response: " + responseString
492         }
493
494         return s
495     }
496
497 }